@variance-authority/core 0.2.0 → 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.
- package/CHANGELOG.md +81 -36
- package/README.md +255 -128
- package/dist/attribute/call-site.d.ts +20 -2
- package/dist/attribute/call-site.js +17 -20
- package/dist/attribute/call-site.js.map +1 -1
- package/dist/attribute/composition.js +67 -11
- package/dist/attribute/composition.js.map +1 -1
- package/dist/attribute/index.d.ts +2 -0
- package/dist/attribute/index.js +1 -0
- package/dist/attribute/index.js.map +1 -1
- package/dist/attribute/landmark.d.ts +97 -0
- package/dist/attribute/landmark.js +144 -0
- package/dist/attribute/landmark.js.map +1 -0
- package/dist/attribute/lexicon.d.ts +25 -1
- package/dist/attribute/lexicon.js +87 -27
- package/dist/attribute/lexicon.js.map +1 -1
- package/dist/attribute/stack.d.ts +62 -1
- package/dist/attribute/stack.js +116 -5
- package/dist/attribute/stack.js.map +1 -1
- package/dist/judge/inspect.js +57 -4
- package/dist/judge/inspect.js.map +1 -1
- package/dist/relate/before.d.ts +137 -0
- package/dist/relate/before.js +155 -0
- package/dist/relate/before.js.map +1 -0
- package/dist/relate/graph.d.ts +28 -11
- package/dist/relate/graph.js +20 -9
- package/dist/relate/graph.js.map +1 -1
- package/dist/relate/index.d.ts +2 -1
- package/dist/relate/index.js +1 -0
- package/dist/relate/index.js.map +1 -1
- package/dist/relate/merkle.d.ts +6 -0
- package/dist/relate/merkle.js +6 -0
- package/dist/relate/merkle.js.map +1 -1
- package/dist/relate/records.d.ts +67 -1
- package/dist/relate/records.js +34 -7
- package/dist/relate/records.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,41 +1,153 @@
|
|
|
1
|
-
<p align="center"><img src="
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
is captured — one decides which baselines the run can reach at all, the other
|
|
32
|
-
decides which subjects are worth reaching for. `core/segment` and `core/share`
|
|
33
|
-
sit outside it in the other direction: they are how a derived answer is written
|
|
34
|
-
down and handed to another machine, and neither knows what it is carrying. Every
|
|
35
|
-
name lives in exactly one group, and the bare specifier holds only the capture
|
|
36
|
-
artifact.
|
|
147
|
+
Every name lives in exactly one group, and the bare `@variance-authority/core`
|
|
148
|
+
specifier exports only the capture artifact.
|
|
37
149
|
|
|
38
|
-
| entrypoint |
|
|
150
|
+
| entrypoint | exports |
|
|
39
151
|
|---|---|
|
|
40
152
|
| `core/format` | what a subject *is*: capture, snapshot, document, identity, hashing |
|
|
41
153
|
| `core/rules` | the versioned opinions: allowlist, applicability, cascade, canonicalization |
|
|
@@ -47,130 +159,144 @@ artifact.
|
|
|
47
159
|
| `core/segment` | columnar bytes: named columns, interned strings, and the validation a decode performs before it believes a file |
|
|
48
160
|
| `core/share` | leaving those bytes where another machine finds them, under a key that is a commit |
|
|
49
161
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 |
|
|
84
210
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const after = shapeValue({ rows: [{ id: 'a', total: 12 }] }, {
|
|
89
|
-
arrayKey: { '/rows': 'id' },
|
|
90
|
-
});
|
|
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.
|
|
91
214
|
|
|
92
|
-
|
|
93
|
-
console.log(deltas[0]?.pointer); // /rows/a/total
|
|
94
|
-
```
|
|
215
|
+
## Compare a rendered subject
|
|
95
216
|
|
|
96
|
-
|
|
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.
|
|
97
221
|
|
|
98
222
|
```ts
|
|
99
|
-
import { attributeRegions, isolateRegions } from '@variance-authority/core/attribute';
|
|
100
|
-
import { diffSnapshots } from '@variance-authority/core/compare';
|
|
101
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';
|
|
102
226
|
|
|
103
|
-
const before = normalize(capture);
|
|
227
|
+
const before = normalize(capture);
|
|
104
228
|
const after = normalize(recapture);
|
|
105
|
-
const diff = diffSnapshots(before, after); // deltas
|
|
229
|
+
const diff = diffSnapshots(before, after); // deltas and roots, no verdict
|
|
106
230
|
|
|
107
|
-
// `mask` is a ChangeMask from the raster tier. Core never opens an image.
|
|
108
231
|
const places = isolateRegions(mask, { cell: 8 }); // pixels → regions
|
|
109
232
|
const named = attributeRegions(places.regions, after, { scale: 2 });
|
|
110
233
|
```
|
|
111
234
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
`
|
|
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.
|
|
116
241
|
|
|
117
|
-
`scale` is device pixels per CSS pixel
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
wrong report. That last step is where regions become components and files.
|
|
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.
|
|
121
245
|
|
|
122
|
-
|
|
246
|
+
Rendered comparisons throw when the two subjects differ, or when their
|
|
247
|
+
observation profiles differ.
|
|
248
|
+
|
|
249
|
+
### Options that change scope
|
|
123
250
|
|
|
124
251
|
| call | useful controls |
|
|
125
252
|
|---|---|
|
|
126
253
|
| `normalize` | `collapseWrappers` removes layout-only wrapper boundaries, `digestText` includes text content, and `sourceRoot` relativizes source locations |
|
|
127
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 |
|
|
128
|
-
| `fingerprintOfMask` | `grid` controls the shape sample and `coverage`
|
|
255
|
+
| `fingerprintOfMask` | `grid` controls the shape sample and `coverage` the minimum occupied share |
|
|
129
256
|
| `compareLocales` / ignore validation | `slack` permits a declared locale distance; `sites` supplies resolved ignore locations and `now` evaluates expiry |
|
|
130
|
-
| `buildDocket` / dependency reach | `sampleSize` limits review examples; `through` selects graph edge kinds, `avoid` names nodes a walk never enters, and `shadows`
|
|
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 |
|
|
131
259
|
| `sharedClosures` | `floor` drops a shared subtree below a node count |
|
|
132
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 |
|
|
133
261
|
| report summaries | `source` maps component names to files when `summarizeAdjudication` or `summarizeFindings` needs an actionable path |
|
|
134
262
|
| screenshot stabilization | `animations` and `caret` are explicit intervention settings; omitted means the caller did not assert either intervention |
|
|
135
263
|
|
|
136
|
-
|
|
264
|
+
## Which input changed
|
|
137
265
|
|
|
138
|
-
`
|
|
139
|
-
|
|
140
|
-
numbers written portably, `undefined` omitted), addresses the text by content,
|
|
141
|
-
and returns a `CapturedValue`; `compareValues` turns two of them into deltas
|
|
142
|
-
carrying a JSON Pointer and a fingerprint, and — like everything else here — no
|
|
143
|
-
verdict.
|
|
266
|
+
`compare` says what changed. `judge` says whether anyone should mind. Between
|
|
267
|
+
them, `partingOf` says which input changed.
|
|
144
268
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
|
148
274
|
|
|
149
|
-
|
|
150
|
-
const deltas = compareValues(baseline, shapeValue(after, { arrayKey: { '/rows': 'id' } }));
|
|
151
|
-
```
|
|
275
|
+
It leads with a slice, so you know whether to open the rest:
|
|
152
276
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
`
|
|
159
|
-
|
|
160
|
-
|
|
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 |
|
|
161
287
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
in the record that the next run reads as removed.
|
|
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.
|
|
165
290
|
|
|
166
|
-
|
|
291
|
+
## Hand an answer to another machine
|
|
167
292
|
|
|
168
|
-
|
|
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.
|
|
169
297
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
facts encode to equal bytes, which is what lets a transport skip an upload — and
|
|
173
|
-
`core/share` moves the bytes:
|
|
298
|
+
Excerpt: `token` is your store's credential and `bytes` is what `core/segment`
|
|
299
|
+
encoded.
|
|
174
300
|
|
|
175
301
|
```ts
|
|
176
302
|
import { httpShare, shareKey } from '@variance-authority/core/share';
|
|
@@ -184,28 +310,29 @@ const share = httpShare({
|
|
|
184
310
|
await share.put(shareKey({ project: 'web', artifact: 'suite-index-v1', commit }), bytes);
|
|
185
311
|
```
|
|
186
312
|
|
|
187
|
-
`endpoint` is a base URL a key is appended to
|
|
188
|
-
request, which is where a bucket's `Authorization` or a deployment's token goes
|
|
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.
|
|
189
315
|
`method` is the verb a write uses — `PUT` for a bucket, `POST` for a deployment
|
|
190
|
-
that routes on it. A presigned base needs
|
|
316
|
+
that routes on it. A presigned base needs only the endpoint.
|
|
191
317
|
|
|
192
318
|
**A share never throws.** A miss, an outage, a permission error and a body
|
|
193
319
|
nobody can parse are one outcome: `get` answers `null` and `put` resolves.
|
|
194
|
-
Everything a share
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
198
324
|
|
|
199
325
|
## What it refuses
|
|
200
326
|
|
|
201
|
-
**Absent is not empty.** Not measured, measured as zero, and unobservable
|
|
202
|
-
three states
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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.
|
|
207
335
|
|
|
208
|
-
|
|
209
|
-
A difference in conditions reported as a difference in the product is a
|
|
210
|
-
confident wrong answer, and the confidence is what makes it expensive.
|
|
336
|
+
---
|
|
211
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
|
|
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
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
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
|
|
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
|
|
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
|
|
156
|
-
*
|
|
157
|
-
* the
|
|
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
|
-
|
|
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;
|