@variance-authority/core 0.1.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 +79 -0
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/artifact.d.ts +47 -0
- package/dist/artifact.js +2 -0
- package/dist/artifact.js.map +1 -0
- package/dist/attribute/because.d.ts +43 -0
- package/dist/attribute/because.js +64 -0
- package/dist/attribute/because.js.map +1 -0
- package/dist/attribute/boundary.d.ts +191 -0
- package/dist/attribute/boundary.js +328 -0
- package/dist/attribute/boundary.js.map +1 -0
- package/dist/attribute/call-site.d.ts +96 -0
- package/dist/attribute/call-site.js +183 -0
- package/dist/attribute/call-site.js.map +1 -0
- package/dist/attribute/component-hash.d.ts +188 -0
- package/dist/attribute/component-hash.js +312 -0
- package/dist/attribute/component-hash.js.map +1 -0
- package/dist/attribute/composition-fixture.d.ts +18 -0
- package/dist/attribute/composition-fixture.js +53 -0
- package/dist/attribute/composition-fixture.js.map +1 -0
- package/dist/attribute/composition.d.ts +182 -0
- package/dist/attribute/composition.js +169 -0
- package/dist/attribute/composition.js.map +1 -0
- package/dist/attribute/control.d.ts +43 -0
- package/dist/attribute/control.js +54 -0
- package/dist/attribute/control.js.map +1 -0
- package/dist/attribute/coverage.d.ts +99 -0
- package/dist/attribute/coverage.js +86 -0
- package/dist/attribute/coverage.js.map +1 -0
- package/dist/attribute/divergence.d.ts +102 -0
- package/dist/attribute/divergence.js +152 -0
- package/dist/attribute/divergence.js.map +1 -0
- package/dist/attribute/index.d.ts +43 -0
- package/dist/attribute/index.js +29 -0
- package/dist/attribute/index.js.map +1 -0
- package/dist/attribute/instability.d.ts +89 -0
- package/dist/attribute/instability.js +183 -0
- package/dist/attribute/instability.js.map +1 -0
- package/dist/attribute/instances.d.ts +145 -0
- package/dist/attribute/instances.js +140 -0
- package/dist/attribute/instances.js.map +1 -0
- package/dist/attribute/locate.d.ts +22 -0
- package/dist/attribute/locate.js +100 -0
- package/dist/attribute/locate.js.map +1 -0
- package/dist/attribute/mask.d.ts +156 -0
- package/dist/attribute/mask.js +201 -0
- package/dist/attribute/mask.js.map +1 -0
- package/dist/attribute/movement.d.ts +211 -0
- package/dist/attribute/movement.js +181 -0
- package/dist/attribute/movement.js.map +1 -0
- package/dist/attribute/region.d.ts +188 -0
- package/dist/attribute/region.js +144 -0
- package/dist/attribute/region.js.map +1 -0
- package/dist/attribute/source-map.d.ts +98 -0
- package/dist/attribute/source-map.js +264 -0
- package/dist/attribute/source-map.js.map +1 -0
- package/dist/attribute/source.d.ts +77 -0
- package/dist/attribute/source.js +95 -0
- package/dist/attribute/source.js.map +1 -0
- package/dist/attribute/stack.d.ts +80 -0
- package/dist/attribute/stack.js +108 -0
- package/dist/attribute/stack.js.map +1 -0
- package/dist/compare/band.d.ts +85 -0
- package/dist/compare/band.js +70 -0
- package/dist/compare/band.js.map +1 -0
- package/dist/compare/cascade.d.ts +45 -0
- package/dist/compare/cascade.js +71 -0
- package/dist/compare/cascade.js.map +1 -0
- package/dist/compare/derive.d.ts +78 -0
- package/dist/compare/derive.js +39 -0
- package/dist/compare/derive.js.map +1 -0
- package/dist/compare/diff/attribution.d.ts +23 -0
- package/dist/compare/diff/attribution.js +244 -0
- package/dist/compare/diff/attribution.js.map +1 -0
- package/dist/compare/diff/compare-nodes.d.ts +15 -0
- package/dist/compare/diff/compare-nodes.js +227 -0
- package/dist/compare/diff/compare-nodes.js.map +1 -0
- package/dist/compare/diff/components.d.ts +20 -0
- package/dist/compare/diff/components.js +76 -0
- package/dist/compare/diff/components.js.map +1 -0
- package/dist/compare/diff/delta.d.ts +165 -0
- package/dist/compare/diff/delta.js +12 -0
- package/dist/compare/diff/delta.js.map +1 -0
- package/dist/compare/diff/index.d.ts +69 -0
- package/dist/compare/diff/index.js +101 -0
- package/dist/compare/diff/index.js.map +1 -0
- package/dist/compare/diff/match.d.ts +37 -0
- package/dist/compare/diff/match.js +167 -0
- package/dist/compare/diff/match.js.map +1 -0
- package/dist/compare/explain.d.ts +23 -0
- package/dist/compare/explain.js +190 -0
- package/dist/compare/explain.js.map +1 -0
- package/dist/compare/holding-diff.d.ts +25 -0
- package/dist/compare/holding-diff.js +74 -0
- package/dist/compare/holding-diff.js.map +1 -0
- package/dist/compare/impact.d.ts +56 -0
- package/dist/compare/impact.js +124 -0
- package/dist/compare/impact.js.map +1 -0
- package/dist/compare/index.d.ts +30 -0
- package/dist/compare/index.js +22 -0
- package/dist/compare/index.js.map +1 -0
- package/dist/compare/instance.d.ts +30 -0
- package/dist/compare/instance.js +145 -0
- package/dist/compare/instance.js.map +1 -0
- package/dist/compare/observability.d.ts +49 -0
- package/dist/compare/observability.js +57 -0
- package/dist/compare/observability.js.map +1 -0
- package/dist/compare/parting.d.ts +179 -0
- package/dist/compare/parting.js +244 -0
- package/dist/compare/parting.js.map +1 -0
- package/dist/compare/slice.d.ts +74 -0
- package/dist/compare/slice.js +79 -0
- package/dist/compare/slice.js.map +1 -0
- package/dist/compare/value.d.ts +60 -0
- package/dist/compare/value.js +111 -0
- package/dist/compare/value.js.map +1 -0
- package/dist/format/accessibility.d.ts +27 -0
- package/dist/format/accessibility.js +17 -0
- package/dist/format/accessibility.js.map +1 -0
- package/dist/format/canonical.d.ts +38 -0
- package/dist/format/canonical.js +65 -0
- package/dist/format/canonical.js.map +1 -0
- package/dist/format/capture.d.ts +212 -0
- package/dist/format/capture.js +2 -0
- package/dist/format/capture.js.map +1 -0
- package/dist/format/document.d.ts +279 -0
- package/dist/format/document.js +65 -0
- package/dist/format/document.js.map +1 -0
- package/dist/format/environment.d.ts +120 -0
- package/dist/format/environment.js +53 -0
- package/dist/format/environment.js.map +1 -0
- package/dist/format/hash.d.ts +45 -0
- package/dist/format/hash.js +41 -0
- package/dist/format/hash.js.map +1 -0
- package/dist/format/holding.d.ts +134 -0
- package/dist/format/holding.js +2 -0
- package/dist/format/holding.js.map +1 -0
- package/dist/format/index.d.ts +39 -0
- package/dist/format/index.js +25 -0
- package/dist/format/index.js.map +1 -0
- package/dist/format/intervention.d.ts +87 -0
- package/dist/format/intervention.js +11 -0
- package/dist/format/intervention.js.map +1 -0
- package/dist/format/profile.d.ts +59 -0
- package/dist/format/profile.js +66 -0
- package/dist/format/profile.js.map +1 -0
- package/dist/format/provenance.d.ts +179 -0
- package/dist/format/provenance.js +222 -0
- package/dist/format/provenance.js.map +1 -0
- package/dist/format/sha256.d.ts +34 -0
- package/dist/format/sha256.js +152 -0
- package/dist/format/sha256.js.map +1 -0
- package/dist/format/snapshot.d.ts +263 -0
- package/dist/format/snapshot.js +2 -0
- package/dist/format/snapshot.js.map +1 -0
- package/dist/format/stabilize.d.ts +190 -0
- package/dist/format/stabilize.js +333 -0
- package/dist/format/stabilize.js.map +1 -0
- package/dist/format/tier.d.ts +19 -0
- package/dist/format/tier.js +11 -0
- package/dist/format/tier.js.map +1 -0
- package/dist/format/value.d.ts +115 -0
- package/dist/format/value.js +182 -0
- package/dist/format/value.js.map +1 -0
- package/dist/format/wiring.d.ts +115 -0
- package/dist/format/wiring.js +60 -0
- package/dist/format/wiring.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/judge/docket.d.ts +85 -0
- package/dist/judge/docket.js +162 -0
- package/dist/judge/docket.js.map +1 -0
- package/dist/judge/fingerprint.d.ts +53 -0
- package/dist/judge/fingerprint.js +144 -0
- package/dist/judge/fingerprint.js.map +1 -0
- package/dist/judge/ignore.d.ts +206 -0
- package/dist/judge/ignore.js +237 -0
- package/dist/judge/ignore.js.map +1 -0
- package/dist/judge/index.d.ts +44 -0
- package/dist/judge/index.js +34 -0
- package/dist/judge/index.js.map +1 -0
- package/dist/judge/inspect.d.ts +148 -0
- package/dist/judge/inspect.js +208 -0
- package/dist/judge/inspect.js.map +1 -0
- package/dist/judge/intent.d.ts +146 -0
- package/dist/judge/intent.js +150 -0
- package/dist/judge/intent.js.map +1 -0
- package/dist/judge/locale.d.ts +111 -0
- package/dist/judge/locale.js +247 -0
- package/dist/judge/locale.js.map +1 -0
- package/dist/judge/scope.d.ts +41 -0
- package/dist/judge/scope.js +47 -0
- package/dist/judge/scope.js.map +1 -0
- package/dist/judge/sensitivity.d.ts +202 -0
- package/dist/judge/sensitivity.js +152 -0
- package/dist/judge/sensitivity.js.map +1 -0
- package/dist/judge/trail.d.ts +122 -0
- package/dist/judge/trail.js +96 -0
- package/dist/judge/trail.js.map +1 -0
- package/dist/judge/verdict.d.ts +39 -0
- package/dist/judge/verdict.js +32 -0
- package/dist/judge/verdict.js.map +1 -0
- package/dist/plan/index.d.ts +12 -0
- package/dist/plan/index.js +11 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/plan/tool.d.ts +85 -0
- package/dist/plan/tool.js +87 -0
- package/dist/plan/tool.js.map +1 -0
- package/dist/relate/graph.d.ts +190 -0
- package/dist/relate/graph.js +234 -0
- package/dist/relate/graph.js.map +1 -0
- package/dist/relate/index.d.ts +16 -0
- package/dist/relate/index.js +16 -0
- package/dist/relate/index.js.map +1 -0
- package/dist/relate/merkle.d.ts +110 -0
- package/dist/relate/merkle.js +246 -0
- package/dist/relate/merkle.js.map +1 -0
- package/dist/relate/reach.d.ts +65 -0
- package/dist/relate/reach.js +111 -0
- package/dist/relate/reach.js.map +1 -0
- package/dist/relate/records.d.ts +129 -0
- package/dist/relate/records.js +112 -0
- package/dist/relate/records.js.map +1 -0
- package/dist/rules/index.d.ts +19 -0
- package/dist/rules/index.js +17 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/normalize/alias.d.ts +50 -0
- package/dist/rules/normalize/alias.js +111 -0
- package/dist/rules/normalize/alias.js.map +1 -0
- package/dist/rules/normalize/cascade.d.ts +85 -0
- package/dist/rules/normalize/cascade.js +324 -0
- package/dist/rules/normalize/cascade.js.map +1 -0
- package/dist/rules/normalize/color.d.ts +30 -0
- package/dist/rules/normalize/color.js +185 -0
- package/dist/rules/normalize/color.js.map +1 -0
- package/dist/rules/normalize/fixture.d.ts +83 -0
- package/dist/rules/normalize/fixture.js +74 -0
- package/dist/rules/normalize/fixture.js.map +1 -0
- package/dist/rules/normalize/index.d.ts +39 -0
- package/dist/rules/normalize/index.js +268 -0
- package/dist/rules/normalize/index.js.map +1 -0
- package/dist/rules/normalize/project.d.ts +42 -0
- package/dist/rules/normalize/project.js +83 -0
- package/dist/rules/normalize/project.js.map +1 -0
- package/dist/rules/normalize/shorthand.d.ts +38 -0
- package/dist/rules/normalize/shorthand.js +388 -0
- package/dist/rules/normalize/shorthand.js.map +1 -0
- package/dist/rules/normalize/value.d.ts +23 -0
- package/dist/rules/normalize/value.js +162 -0
- package/dist/rules/normalize/value.js.map +1 -0
- package/dist/rules/normalize/wrapper.d.ts +16 -0
- package/dist/rules/normalize/wrapper.js +137 -0
- package/dist/rules/normalize/wrapper.js.map +1 -0
- package/dist/rules/ruleset.d.ts +93 -0
- package/dist/rules/ruleset.js +175 -0
- package/dist/rules/ruleset.js.map +1 -0
- package/mark.svg +30 -0
- package/package.json +69 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @variance-authority/core
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5c34e6d: Say which input moved, not just which tag did
|
|
8
|
+
|
|
9
|
+
`partingOf(baseline, candidate)` climbs from a set of deltas to the component
|
|
10
|
+
boundary that owns them and names the input that carried the decision:
|
|
11
|
+
`Cart chose differently — useState #0 moved`, with `Summary` reported as having
|
|
12
|
+
been handed a different `expanded` and hanging under that line as a
|
|
13
|
+
manifestation rather than as a second finding. `explainParting` renders that
|
|
14
|
+
as lines. Two runs of one page that differ in a `<p>` where the other has a
|
|
15
|
+
`<span>` previously produced a structural delta and nothing to say about it;
|
|
16
|
+
the tag is now the symptom and the hook cell is the report.
|
|
17
|
+
|
|
18
|
+
`holdingOf` from `@variance-authority/react` supplies the evidence — props,
|
|
19
|
+
context values and hook cells as digests, per boundary, including
|
|
20
|
+
`useSyncExternalStore` snapshots so a store that moved outside React is
|
|
21
|
+
distinguished from a component that decided differently on its own. Values are
|
|
22
|
+
never carried, only digests, and a holding reaches no hash: `renderHash`,
|
|
23
|
+
`structureHash`, `styleHash`, band-exact component digests and
|
|
24
|
+
`componentInstances` are all unmoved by it, the same bargain `styleProvenance`
|
|
25
|
+
makes. `collect` in `@variance-authority/dom` takes `holdingOf` as a
|
|
26
|
+
caller-supplied reader, opt in separately from `wiringOf`, and `capture` in
|
|
27
|
+
`@variance-authority/unit-test` now accepts `provenanceOf`, `wiringOf` and
|
|
28
|
+
`holdingOf` so a unit test can read one.
|
|
29
|
+
|
|
30
|
+
A boundary whose own input could not be read is reported as `unread` and never
|
|
31
|
+
as nondeterminism: `undetermined` is reserved for a component whose every input
|
|
32
|
+
was read and agreed. A wrapper that roots a component boundary no longer
|
|
33
|
+
collapses, because collapsing it discarded the holding — the cost is that a run
|
|
34
|
+
reading holdings keeps wrappers a run without them removes, which is why both
|
|
35
|
+
sides of a comparison must be read the same way.
|
|
36
|
+
|
|
37
|
+
A component that ran no hooks is read as having run none, rather than as
|
|
38
|
+
unreadable. React writes `_debugHookTypes = null` on every fiber and fills it on
|
|
39
|
+
the first hook call, so the property's absence is the only silence — and
|
|
40
|
+
collapsing the two reported the one shape most worth calling nondeterministic, a
|
|
41
|
+
component with no props and no hooks that renders differently twice, as
|
|
42
|
+
something nothing could be said about.
|
|
43
|
+
|
|
44
|
+
`PartedBoundary.moved` names the properties the owned deltas named — `color`,
|
|
45
|
+
`padding-top`, `width` — and `explainParting` spends them on the delta line.
|
|
46
|
+
That is the last joint of the chain the rungs climb: a hook cell moved, a prop
|
|
47
|
+
carried it down, and this is what the prop turned into on the page. A count and
|
|
48
|
+
a band stop one link short of what somebody chasing a visual regression is
|
|
49
|
+
trying to name.
|
|
50
|
+
|
|
51
|
+
`Parting.slice` answers the question asked before which input moved: is this
|
|
52
|
+
worth opening. Three facts are read independently — did the component tree
|
|
53
|
+
move, did any input move, did the output move — and the combinations collapse
|
|
54
|
+
to six sentences. `refactor` is the one that pays for the rest: a component
|
|
55
|
+
tree that moved while the page did not is the receipt a refactor never gets,
|
|
56
|
+
since a pixel differ can say the screenshots match and nothing about what was
|
|
57
|
+
rewritten underneath. `flake` is `settled`'s opposite number and is refused on
|
|
58
|
+
silence — a run that read no boundary reports `unread`, never `flake`.
|
|
59
|
+
|
|
60
|
+
`explainParting` leads with that line, and stops enumerating manifestations
|
|
61
|
+
past three: one input at a fork can put a boundary on every component beneath
|
|
62
|
+
it, and nine lines carrying one decision bury the one line worth reading.
|
|
63
|
+
|
|
64
|
+
The whole layer is documented in `docs/parting.md`: the six slices, the seven
|
|
65
|
+
rungs, what a holding carries and what it deliberately does not.
|
|
66
|
+
|
|
67
|
+
### Patch Changes
|
|
68
|
+
|
|
69
|
+
- e8fee66: Refuse a value whose state is not in its own enumerable keys.
|
|
70
|
+
|
|
71
|
+
`shapeValue` documented a `Date` as refused and encoded it instead. `typeof`
|
|
72
|
+
answers `object`, `Object.entries` answers empty, and the canonical text came
|
|
73
|
+
out `{}` — so a snapshot holding a timestamp addressed to the digest of an empty
|
|
74
|
+
object, and a later run comparing a different timestamp reported unchanged. The
|
|
75
|
+
same hole swallowed `Map`, `Set`, `URL`, and `RegExp`.
|
|
76
|
+
|
|
77
|
+
An object with no own enumerable keys and a prototype other than `Object`'s is
|
|
78
|
+
now refused by name at its pointer. A genuinely empty object is still a value,
|
|
79
|
+
and an instance carrying its own fields still serializes them.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Machine Garden
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<p align="center"><img src="./mark.svg" alt="Variance Authority mark" width="72"></p>
|
|
2
|
+
|
|
3
|
+
# @variance-authority/core
|
|
4
|
+
|
|
5
|
+
> The Variance Authority format, rules, comparison, attribution and verdicts. Pure data in, pure data out, no DOM and no I/O.
|
|
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.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install --save-dev @variance-authority/core
|
|
14
|
+
```
|
|
15
|
+
## Use this package when
|
|
16
|
+
|
|
17
|
+
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.
|
|
26
|
+
|
|
27
|
+
## Entrypoints
|
|
28
|
+
|
|
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.
|
|
34
|
+
|
|
35
|
+
| entrypoint | holds |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `core/format` | what a subject *is*: capture, snapshot, document, identity, hashing |
|
|
38
|
+
| `core/rules` | the versioned opinions: allowlist, applicability, cascade, canonicalization |
|
|
39
|
+
| `core/compare` | two snapshots become deltas — and **no verdict** |
|
|
40
|
+
| `core/attribute` | a position becomes a component becomes a file |
|
|
41
|
+
| `core/judge` | policy: verdicts, intent claims, ignores, the docket a reader is handed |
|
|
42
|
+
| `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
|
+
| `core/relate` | what rests on what: a file graph in adjacency form, the components a change reaches, and a closure digest over each one |
|
|
44
|
+
|
|
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.
|
|
70
|
+
|
|
71
|
+
## Smallest working path: compare a value
|
|
72
|
+
|
|
73
|
+
Value comparison needs no host setup and returns paths and fingerprints, not a
|
|
74
|
+
pass/fail verdict:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { compareValues, shapeValue } from '@variance-authority/core';
|
|
78
|
+
|
|
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
|
|
96
|
+
const after = normalize(recapture);
|
|
97
|
+
const diff = diffSnapshots(before, after); // deltas, roots, no verdict
|
|
98
|
+
|
|
99
|
+
// `mask` is a ChangeMask from the raster tier. Core never opens an image.
|
|
100
|
+
const places = isolateRegions(mask, { cell: 8 }); // pixels → regions
|
|
101
|
+
const named = attributeRegions(places.regions, after, { scale: 2 });
|
|
102
|
+
```
|
|
103
|
+
|
|
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.
|
|
108
|
+
|
|
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.
|
|
113
|
+
|
|
114
|
+
The options that change scope are explicit at the call site:
|
|
115
|
+
|
|
116
|
+
| call | useful controls |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `normalize` | `collapseWrappers` removes layout-only wrapper boundaries, `digestText` includes text content, and `sourceRoot` relativizes source locations |
|
|
119
|
+
| `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 |
|
|
121
|
+
| `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 |
|
|
123
|
+
| report summaries | `source` maps component names to files when `summarizeAdjudication` or `summarizeFindings` needs an actionable path |
|
|
124
|
+
| screenshot stabilization | `animations` and `caret` are explicit intervention settings; omitted means the caller did not assert either intervention |
|
|
125
|
+
|
|
126
|
+
### A subject that was never rendered
|
|
127
|
+
|
|
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.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { shapeValue, compareValues } from '@variance-authority/core';
|
|
137
|
+
|
|
138
|
+
const baseline = shapeValue(before, { arrayKey: { '/rows': 'id' } });
|
|
139
|
+
const deltas = compareValues(baseline, shapeValue(after, { arrayKey: { '/rows': 'id' } }));
|
|
140
|
+
```
|
|
141
|
+
|
|
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.
|
|
150
|
+
|
|
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.
|
|
154
|
+
|
|
155
|
+
Rendered comparisons also throw when the subjects or observation profiles differ.
|
|
156
|
+
|
|
157
|
+
## What it refuses
|
|
158
|
+
|
|
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.
|
|
165
|
+
|
|
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.
|
|
169
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SourceIndex } from './attribute/source.js';
|
|
2
|
+
import type { AccessibilitySnapshot, CapturedValue, Raster, RenderDocument, SemanticSnapshot, SubjectRef } from './format/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Material already captured from a host, before retention or reporting.
|
|
5
|
+
*
|
|
6
|
+
* Three arms, and the third is the one that is not a rendering at all: a value
|
|
7
|
+
* has no viewport, no placement and nothing to paint, and it reaches the same
|
|
8
|
+
* attribution path as the other two (ADR-0044). Every existing reader narrows
|
|
9
|
+
* with `=== 'document'`, so a value arm is not a compile error anywhere — which
|
|
10
|
+
* means each of those readers owes an explicit refusal rather than a silent drop.
|
|
11
|
+
*/
|
|
12
|
+
export type CaptureMaterial = {
|
|
13
|
+
readonly kind: 'document';
|
|
14
|
+
readonly document: RenderDocument;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'raster';
|
|
17
|
+
readonly raster: Raster;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: 'value';
|
|
20
|
+
readonly value: CapturedValue;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The acquisition boundary shared by host adapters.
|
|
24
|
+
*
|
|
25
|
+
* The material decides whether a renderer is needed. Semantic and source
|
|
26
|
+
* evidence stay beside it so an in-place raster and a deferred document reach
|
|
27
|
+
* the same attribution path without inventing host-specific result types.
|
|
28
|
+
*/
|
|
29
|
+
export interface CaptureArtifact {
|
|
30
|
+
readonly artifactVersion: 1;
|
|
31
|
+
readonly subject: SubjectRef;
|
|
32
|
+
readonly material: CaptureMaterial;
|
|
33
|
+
readonly snapshot?: SemanticSnapshot;
|
|
34
|
+
/** Browser-native accessibility evidence from the same stabilized mount. */
|
|
35
|
+
readonly accessibility?: AccessibilitySnapshot;
|
|
36
|
+
readonly source?: SourceIndex;
|
|
37
|
+
readonly stabilization?: readonly string[];
|
|
38
|
+
readonly attempt?: {
|
|
39
|
+
readonly retry: number;
|
|
40
|
+
readonly repeat: number;
|
|
41
|
+
readonly shard?: {
|
|
42
|
+
readonly index: number;
|
|
43
|
+
readonly total: number;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=artifact.d.ts.map
|
package/dist/artifact.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifact.js","sourceRoot":"","sources":["../src/artifact.ts"],"names":[],"mappings":"","sourcesContent":["import type { SourceIndex } from './attribute/source.js';\nimport type {\n AccessibilitySnapshot,\n CapturedValue,\n Raster,\n RenderDocument,\n SemanticSnapshot,\n SubjectRef,\n} from './format/index.js';\n\n/**\n * Material already captured from a host, before retention or reporting.\n *\n * Three arms, and the third is the one that is not a rendering at all: a value\n * has no viewport, no placement and nothing to paint, and it reaches the same\n * attribution path as the other two (ADR-0044). Every existing reader narrows\n * with `=== 'document'`, so a value arm is not a compile error anywhere — which\n * means each of those readers owes an explicit refusal rather than a silent drop.\n */\nexport type CaptureMaterial =\n | { readonly kind: 'document'; readonly document: RenderDocument }\n | { readonly kind: 'raster'; readonly raster: Raster }\n | { readonly kind: 'value'; readonly value: CapturedValue };\n\n/**\n * The acquisition boundary shared by host adapters.\n *\n * The material decides whether a renderer is needed. Semantic and source\n * evidence stay beside it so an in-place raster and a deferred document reach\n * the same attribution path without inventing host-specific result types.\n */\nexport interface CaptureArtifact {\n readonly artifactVersion: 1;\n readonly subject: SubjectRef;\n readonly material: CaptureMaterial;\n readonly snapshot?: SemanticSnapshot;\n /** Browser-native accessibility evidence from the same stabilized mount. */\n readonly accessibility?: AccessibilitySnapshot;\n readonly source?: SourceIndex;\n readonly stabilization?: readonly string[];\n readonly attempt?: {\n readonly retry: number;\n readonly repeat: number;\n readonly shard?: { readonly index: number; readonly total: number };\n };\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one sentence a movement carries, written once for both readers.
|
|
3
|
+
*
|
|
4
|
+
* Every rung ends in prose because the category is not the answer — *upstream*
|
|
5
|
+
* tells a reviewer nothing, `ProductCard` was edited and reaches it through
|
|
6
|
+
* `Card` sends them to a file. The sentence is written here, shipped to the
|
|
7
|
+
* terminal and to the review page, and rendered rather than rewritten at the far
|
|
8
|
+
* end: a second sentence composed for the web is a second thing that can
|
|
9
|
+
* disagree with the first, and the disagreement would be between a reviewer's
|
|
10
|
+
* screen and their CI log.
|
|
11
|
+
*
|
|
12
|
+
* Names the sentence claims things about are wrapped in backticks. That is for
|
|
13
|
+
* the terminal, and the page turns each pair into `code` rather than reprinting
|
|
14
|
+
* the punctuation.
|
|
15
|
+
*/
|
|
16
|
+
import type { Site } from './composition.js';
|
|
17
|
+
import type { Ancestor, Evidence } from './movement.js';
|
|
18
|
+
/**
|
|
19
|
+
* The upstream sentence, which is the one a reviewer acts on.
|
|
20
|
+
*
|
|
21
|
+
* It says three things and the third is the reason the first two are worth
|
|
22
|
+
* printing: an edited component reaches this one, this one's own file is not in
|
|
23
|
+
* the change set, and therefore what moved here is what it was handed. Every
|
|
24
|
+
* rung above this has already been tried, so *its own code* and *a token it
|
|
25
|
+
* reads* are both ruled out by the time this speaks.
|
|
26
|
+
*/
|
|
27
|
+
export declare function becauseUpstream(ancestor: Ancestor): string;
|
|
28
|
+
/**
|
|
29
|
+
* Why nothing explains it, and how much the suite could say about that.
|
|
30
|
+
*
|
|
31
|
+
* The three endings are three different findings and were one sentence. An empty
|
|
32
|
+
* control group has two causes — the component renders nowhere else with these
|
|
33
|
+
* inputs, or it renders elsewhere and moved in every one of them — and they point
|
|
34
|
+
* opposite ways. The first is a suite with nothing to compare against. The second
|
|
35
|
+
* *is* the comparison, and a component that moved in all six of its renders with
|
|
36
|
+
* no file, token or ancestor behind it is the strongest unexplained this ladder
|
|
37
|
+
* can produce.
|
|
38
|
+
*/
|
|
39
|
+
export declare function unexplainedBecause(movement: {
|
|
40
|
+
readonly component: string;
|
|
41
|
+
readonly held: readonly Site[];
|
|
42
|
+
}, considered: number, evidence: Evidence): string;
|
|
43
|
+
//# sourceMappingURL=because.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one sentence a movement carries, written once for both readers.
|
|
3
|
+
*
|
|
4
|
+
* Every rung ends in prose because the category is not the answer — *upstream*
|
|
5
|
+
* tells a reviewer nothing, `ProductCard` was edited and reaches it through
|
|
6
|
+
* `Card` sends them to a file. The sentence is written here, shipped to the
|
|
7
|
+
* terminal and to the review page, and rendered rather than rewritten at the far
|
|
8
|
+
* end: a second sentence composed for the web is a second thing that can
|
|
9
|
+
* disagree with the first, and the disagreement would be between a reviewer's
|
|
10
|
+
* screen and their CI log.
|
|
11
|
+
*
|
|
12
|
+
* Names the sentence claims things about are wrapped in backticks. That is for
|
|
13
|
+
* the terminal, and the page turns each pair into `code` rather than reprinting
|
|
14
|
+
* the punctuation.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The upstream sentence, which is the one a reviewer acts on.
|
|
18
|
+
*
|
|
19
|
+
* It says three things and the third is the reason the first two are worth
|
|
20
|
+
* printing: an edited component reaches this one, this one's own file is not in
|
|
21
|
+
* the change set, and therefore what moved here is what it was handed. Every
|
|
22
|
+
* rung above this has already been tried, so *its own code* and *a token it
|
|
23
|
+
* reads* are both ruled out by the time this speaks.
|
|
24
|
+
*/
|
|
25
|
+
export function becauseUpstream(ancestor) {
|
|
26
|
+
const reaches = ancestor.through.length === 0 ? 'mounts it' : `reaches it through ${chain(ancestor.through)}`;
|
|
27
|
+
return (`\`${ancestor.name}\` was edited and ${reaches}; nothing edited its own file, ` +
|
|
28
|
+
`so it moved on what it was given`);
|
|
29
|
+
}
|
|
30
|
+
/** The components in between, named while there are few enough to be worth naming. */
|
|
31
|
+
function chain(through) {
|
|
32
|
+
const named = through.map((name) => `\`${name}\``);
|
|
33
|
+
if (named.length === 1)
|
|
34
|
+
return named[0] ?? '';
|
|
35
|
+
if (named.length > 3)
|
|
36
|
+
return `${named.slice(0, 3).join(', ')} and ${String(named.length - 3)} more`;
|
|
37
|
+
return `${named.slice(0, -1).join(', ')} and ${named[named.length - 1] ?? ''}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Why nothing explains it, and how much the suite could say about that.
|
|
41
|
+
*
|
|
42
|
+
* The three endings are three different findings and were one sentence. An empty
|
|
43
|
+
* control group has two causes — the component renders nowhere else with these
|
|
44
|
+
* inputs, or it renders elsewhere and moved in every one of them — and they point
|
|
45
|
+
* opposite ways. The first is a suite with nothing to compare against. The second
|
|
46
|
+
* *is* the comparison, and a component that moved in all six of its renders with
|
|
47
|
+
* no file, token or ancestor behind it is the strongest unexplained this ladder
|
|
48
|
+
* can produce.
|
|
49
|
+
*/
|
|
50
|
+
export function unexplainedBecause(movement, considered, evidence) {
|
|
51
|
+
if (evidence.changed === undefined) {
|
|
52
|
+
return 'nothing was asked about what changed, so nothing here explains it — run with `--against` to reach the first rung';
|
|
53
|
+
}
|
|
54
|
+
const unexplained = 'no file, token or ancestor explains it, and';
|
|
55
|
+
if (considered === 0) {
|
|
56
|
+
return `${unexplained} it renders nowhere else in this run with these props to compare against`;
|
|
57
|
+
}
|
|
58
|
+
if (movement.held.length === 0) {
|
|
59
|
+
return `${unexplained} every other render of it with these props moved here too`;
|
|
60
|
+
}
|
|
61
|
+
return (`${unexplained} the same component with the same props held in ` +
|
|
62
|
+
`${movement.held.length} of ${considered} other place(s) in this run`);
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=because.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"because.js","sourceRoot":"","sources":["../../src/attribute/because.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,QAAkB;IAChD,MAAM,OAAO,GACX,QAAQ,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,sBAAsB,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;IAEhG,OAAO,CACL,KAAK,QAAQ,CAAC,IAAI,qBAAqB,OAAO,iCAAiC;QAC/E,kCAAkC,CACnC,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,SAAS,KAAK,CAAC,OAA0B;IACvC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;IACnD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC;IACpG,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;AACjF,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAAwE,EACxE,UAAkB,EAClB,QAAkB;IAElB,IAAI,QAAQ,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,kHAAkH,CAAC;IAC5H,CAAC;IAED,MAAM,WAAW,GAAG,6CAA6C,CAAC;IAElE,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,GAAG,WAAW,0EAA0E,CAAC;IAClG,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,GAAG,WAAW,2DAA2D,CAAC;IACnF,CAAC;IAED,OAAO,CACL,GAAG,WAAW,kDAAkD;QAChE,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,OAAO,UAAU,6BAA6B,CACtE,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The one sentence a movement carries, written once for both readers.\n *\n * Every rung ends in prose because the category is not the answer — *upstream*\n * tells a reviewer nothing, `ProductCard` was edited and reaches it through\n * `Card` sends them to a file. The sentence is written here, shipped to the\n * terminal and to the review page, and rendered rather than rewritten at the far\n * end: a second sentence composed for the web is a second thing that can\n * disagree with the first, and the disagreement would be between a reviewer's\n * screen and their CI log.\n *\n * Names the sentence claims things about are wrapped in backticks. That is for\n * the terminal, and the page turns each pair into `code` rather than reprinting\n * the punctuation.\n */\n\nimport type { Site } from './composition.js';\nimport type { Ancestor, Evidence } from './movement.js';\n\n/**\n * The upstream sentence, which is the one a reviewer acts on.\n *\n * It says three things and the third is the reason the first two are worth\n * printing: an edited component reaches this one, this one's own file is not in\n * the change set, and therefore what moved here is what it was handed. Every\n * rung above this has already been tried, so *its own code* and *a token it\n * reads* are both ruled out by the time this speaks.\n */\nexport function becauseUpstream(ancestor: Ancestor): string {\n const reaches =\n ancestor.through.length === 0 ? 'mounts it' : `reaches it through ${chain(ancestor.through)}`;\n\n return (\n `\\`${ancestor.name}\\` was edited and ${reaches}; nothing edited its own file, ` +\n `so it moved on what it was given`\n );\n}\n\n/** The components in between, named while there are few enough to be worth naming. */\nfunction chain(through: readonly string[]): string {\n const named = through.map((name) => `\\`${name}\\``);\n if (named.length === 1) return named[0] ?? '';\n if (named.length > 3) return `${named.slice(0, 3).join(', ')} and ${String(named.length - 3)} more`;\n return `${named.slice(0, -1).join(', ')} and ${named[named.length - 1] ?? ''}`;\n}\n\n/**\n * Why nothing explains it, and how much the suite could say about that.\n *\n * The three endings are three different findings and were one sentence. An empty\n * control group has two causes — the component renders nowhere else with these\n * inputs, or it renders elsewhere and moved in every one of them — and they point\n * opposite ways. The first is a suite with nothing to compare against. The second\n * *is* the comparison, and a component that moved in all six of its renders with\n * no file, token or ancestor behind it is the strongest unexplained this ladder\n * can produce.\n */\nexport function unexplainedBecause(\n movement: { readonly component: string; readonly held: readonly Site[] },\n considered: number,\n evidence: Evidence,\n): string {\n if (evidence.changed === undefined) {\n return 'nothing was asked about what changed, so nothing here explains it — run with `--against` to reach the first rung';\n }\n\n const unexplained = 'no file, token or ancestor explains it, and';\n\n if (considered === 0) {\n return `${unexplained} it renders nowhere else in this run with these props to compare against`;\n }\n\n if (movement.held.length === 0) {\n return `${unexplained} every other render of it with these props moved here too`;\n }\n\n return (\n `${unexplained} the same component with the same props held in ` +\n `${movement.held.length} of ${considered} other place(s) in this run`\n );\n}\n"]}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { CanonicalValue } from '../format/canonical.js';
|
|
2
|
+
import type { Rect } from '../format/capture.js';
|
|
3
|
+
import type { Digest } from '../format/hash.js';
|
|
4
|
+
import type { SemanticNode } from '../format/snapshot.js';
|
|
5
|
+
/**
|
|
6
|
+
* Where one component's nodes stop and the next component's begin.
|
|
7
|
+
*
|
|
8
|
+
* Two relations run upward out of a rendered node and they are not the same
|
|
9
|
+
* relation. **Parent** is what is above it — React's `return` chain, the boxes
|
|
10
|
+
* it ended up inside. **Owner** is what rendered it — React's `_debugOwner`, the
|
|
11
|
+
* code that wrote the element. A layout wrapper is the parent of everything
|
|
12
|
+
* handed to it and the owner of none of it.
|
|
13
|
+
*
|
|
14
|
+
* Both are needed here, for different halves of the same answer:
|
|
15
|
+
*
|
|
16
|
+
* - **Parent decides nesting.** A boundary owns a contiguous region of the
|
|
17
|
+
* document, and what encloses that region is a fact about the tree. Reading
|
|
18
|
+
* the owner for this would produce a boundary set that is not a partition.
|
|
19
|
+
* - **Owner decides membership and naming.** A component's own content is what
|
|
20
|
+
* *it* wrote. Content it was handed is a hole in its output — present, sized,
|
|
21
|
+
* positioned by it, and authored somewhere else.
|
|
22
|
+
*
|
|
23
|
+
* Reading the parent for both — which is what a walk over `owners[0]` does — has
|
|
24
|
+
* two consequences, and they are the two shapes a design system is made of:
|
|
25
|
+
*
|
|
26
|
+
* - **A component that renders only components disappears.** It authors no host
|
|
27
|
+
* node, so it is never any node's nearest enclosing composite, so it is a
|
|
28
|
+
* boundary nowhere. That is every variant wrapper (`DangerButton` returning a
|
|
29
|
+
* `Button`) and every page-level assembly. The information is not missing —
|
|
30
|
+
* the composite is in the chain, one rung up — it is discarded by reading only
|
|
31
|
+
* the head of it. {@link boundaries} enters every rung the chain crosses.
|
|
32
|
+
* - **A container absorbs its caller's content.** `Card` renders one `div` and
|
|
33
|
+
* whatever it was given; hashed by enclosure, its digest moves whenever a
|
|
34
|
+
* caller passes something else, while its props digest — which excludes
|
|
35
|
+
* `children` — says its inputs held. {@link shapeOf} names a child boundary
|
|
36
|
+
* only when this component placed it, and leaves an anonymous hole otherwise.
|
|
37
|
+
*
|
|
38
|
+
* The owner half is a development-build artefact: React populates `_debugOwner`
|
|
39
|
+
* from `element._owner`, and a production bundle does not. Absent, both rules
|
|
40
|
+
* degrade to naming everything, which is the enclosure answer — coarser, never
|
|
41
|
+
* wrong in a new direction.
|
|
42
|
+
*
|
|
43
|
+
* Nothing here is exported from the package. The callers are siblings.
|
|
44
|
+
*/
|
|
45
|
+
/** A node whose provenance chain broke. Not a filler category; see `RootKind`. */
|
|
46
|
+
export declare const UNATTRIBUTED = "(unattributed)";
|
|
47
|
+
/**
|
|
48
|
+
* Properties whose *computed* value is layout output rather than authored input.
|
|
49
|
+
*
|
|
50
|
+
* The distinction this list exists for is not stylistic. Under a profile with a
|
|
51
|
+
* layout engine the snapshot carries the engine's resolved values, so a block
|
|
52
|
+
* element's computed `height` is whatever its contents made it — and a button
|
|
53
|
+
* two levels down growing by six pixels moves the computed height of every
|
|
54
|
+
* ancestor. Hashed as *style*, that reports every enclosing component as having
|
|
55
|
+
* changed, which is precisely the "area ranks the displaced above the displacer"
|
|
56
|
+
* failure the cause hashes exist to fix, arriving through a different door.
|
|
57
|
+
*
|
|
58
|
+
* Measured on `cases/storybook-case`: one padding edit inside `Button` made
|
|
59
|
+
* `Tokens`, `Stack`, `Card` and the unattributed root all report a moved style
|
|
60
|
+
* hash, so *every* component in every affected story was named a cause.
|
|
61
|
+
*
|
|
62
|
+
* They are folded into `geometry` instead, where "this component's box is a
|
|
63
|
+
* different size" already belongs and where it correctly does not make a cause.
|
|
64
|
+
* Under a profile *without* layout there are no computed values, so these are
|
|
65
|
+
* authored declarations like any other and stay in `style` — that tier has no
|
|
66
|
+
* `geometry` digest to move them to, and a declared `width: 100px` really is the
|
|
67
|
+
* component's own content.
|
|
68
|
+
*/
|
|
69
|
+
export declare const LAYOUT_OUTPUT: ReadonlySet<string>;
|
|
70
|
+
export interface Boundary {
|
|
71
|
+
readonly node: SemanticNode;
|
|
72
|
+
readonly component: string;
|
|
73
|
+
/** The enclosing boundary's component, absent on the subject root. */
|
|
74
|
+
readonly within?: string;
|
|
75
|
+
/** Boundaries between this one and the subject root. The root is `0`. */
|
|
76
|
+
readonly depth: number;
|
|
77
|
+
/**
|
|
78
|
+
* This component's rung in the node's ownership stack.
|
|
79
|
+
*
|
|
80
|
+
* Distinct from `depth`, which counts enclosing boundaries. They diverge
|
|
81
|
+
* wherever a subject root has no provenance: the `(unattributed)` container is
|
|
82
|
+
* a boundary at depth 0 holding a stack of one, and the application inside it
|
|
83
|
+
* starts again at rung 0 while sitting at depth 1.
|
|
84
|
+
*/
|
|
85
|
+
readonly rung: number;
|
|
86
|
+
/**
|
|
87
|
+
* Digest of the props this boundary received, from its own `OwnerFrame`.
|
|
88
|
+
*
|
|
89
|
+
* Read at this rung rather than from `owners[0]`, which is the innermost
|
|
90
|
+
* component and belongs to whichever boundary is deepest at this node — the
|
|
91
|
+
* distinction only exists because several boundaries can share a root.
|
|
92
|
+
*/
|
|
93
|
+
readonly props?: Digest;
|
|
94
|
+
/** The component whose JSX placed this one. Absent on a production build. */
|
|
95
|
+
readonly placedBy?: string;
|
|
96
|
+
}
|
|
97
|
+
/** Whether a node's own content belongs to this boundary rather than one below. */
|
|
98
|
+
export declare function holds(boundary: Boundary, node: SemanticNode): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Every boundary root in the subject, in document order.
|
|
101
|
+
*
|
|
102
|
+
* A boundary opens at each rung a node's ownership stack adds to its parent's,
|
|
103
|
+
* plus the subject root. Several can open at one node — a page assembly, the
|
|
104
|
+
* card it returns and the stack inside that card all begin at the same element —
|
|
105
|
+
* which is what makes a component that renders only components visible.
|
|
106
|
+
*
|
|
107
|
+
* Collected in a separate pre-order pass rather than discovered during hashing,
|
|
108
|
+
* so that instance order is document order exactly. Discovering them while
|
|
109
|
+
* walking would order them by boundary depth instead, and "the second instance"
|
|
110
|
+
* would mean something different in a nested tree.
|
|
111
|
+
*/
|
|
112
|
+
export declare function boundaries(root: SemanticNode): readonly Boundary[];
|
|
113
|
+
export interface Shape {
|
|
114
|
+
readonly structure: CanonicalValue;
|
|
115
|
+
readonly semantics: CanonicalValue;
|
|
116
|
+
readonly text: CanonicalValue;
|
|
117
|
+
readonly style: CanonicalValue;
|
|
118
|
+
readonly geometry: CanonicalValue;
|
|
119
|
+
/**
|
|
120
|
+
* How the framework holds this boundary, per node that reports it.
|
|
121
|
+
*
|
|
122
|
+
* Its own band because it answers its own question. `structure` through
|
|
123
|
+
* `geometry` all read what the renderer produced; this reads what the
|
|
124
|
+
* component *is* — its hook shape, its wrappers, the contexts it subscribes
|
|
125
|
+
* to, the keys it is reconciled under. Two components can agree on all five
|
|
126
|
+
* content bands and disagree here, and when they do, they behave differently
|
|
127
|
+
* under every change that follows.
|
|
128
|
+
*/
|
|
129
|
+
readonly wiring: CanonicalValue;
|
|
130
|
+
/**
|
|
131
|
+
* The box this boundary's root node occupies. Not part of any digest.
|
|
132
|
+
*
|
|
133
|
+
* Beside the digests rather than inside `geometry`, which is the whole subtree:
|
|
134
|
+
* this is the only thing on the record that can say a control grew eight pixels
|
|
135
|
+
* rather than merely stopped being what it was. `null` where nothing was laid out.
|
|
136
|
+
*/
|
|
137
|
+
readonly box: Rect | null;
|
|
138
|
+
/** Child boundaries encountered, in document order. Not part of any digest. */
|
|
139
|
+
readonly renders: readonly string[];
|
|
140
|
+
/** Nodes this boundary owns, counting its own root. Not part of any digest. */
|
|
141
|
+
readonly nodes: number;
|
|
142
|
+
/**
|
|
143
|
+
* Custom properties this boundary's own nodes resolved through, sorted.
|
|
144
|
+
*
|
|
145
|
+
* Not part of any digest — the *values* are already inside `style`, and the
|
|
146
|
+
* names are carried beside it so a token that moved can be joined to the
|
|
147
|
+
* components that read it. Off the boundary rather than off the subject on
|
|
148
|
+
* purpose: every subject on a themed page resolves through every token in the
|
|
149
|
+
* theme, so a subject-level list names them all and explains nothing.
|
|
150
|
+
*/
|
|
151
|
+
readonly tokens: readonly string[];
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* How a value that may carry a structural alias is rewritten before hashing.
|
|
155
|
+
*
|
|
156
|
+
* `undefined` for the per-name hashes, which have always hashed the alias the
|
|
157
|
+
* normalizer assigned and must keep doing so byte for byte. Supplied by the
|
|
158
|
+
* per-instance hashes, which need a boundary-local alias space — see
|
|
159
|
+
* {@link ./instances.js}, where the argument for it lives.
|
|
160
|
+
*/
|
|
161
|
+
export interface Rename {
|
|
162
|
+
readonly attribute: (name: string, value: string) => string;
|
|
163
|
+
readonly style: (value: string) => string;
|
|
164
|
+
readonly alias: (alias: string) => string;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* One boundary's content, stopping at nested boundaries.
|
|
168
|
+
*
|
|
169
|
+
* Where a child belongs to another component, this records a placeholder rather
|
|
170
|
+
* than descending. That is the whole design: a component's hash moves when *its
|
|
171
|
+
* own* code changes, and what a child renders internally is not that. Hashing
|
|
172
|
+
* whole subtrees instead would move every ancestor on any leaf edit, and the
|
|
173
|
+
* page root would change on every commit.
|
|
174
|
+
*
|
|
175
|
+
* **The placeholder names the child only when this component placed it.** A
|
|
176
|
+
* component that wrote `<Stack>` owns that choice, and swapping it for a `<Card>`
|
|
177
|
+
* is its own change; a container that was handed a `<Stack>` chose nothing, and
|
|
178
|
+
* naming it would make the container's digest a function of its callers. So a
|
|
179
|
+
* slot is anonymous, and `Card` renders the same bytes on every page that uses
|
|
180
|
+
* it, whatever it was given. The cost is stated in ADR-0035: the *number* of
|
|
181
|
+
* slotted children still reaches the digest, so a caller passing three where it
|
|
182
|
+
* passed two moves the container.
|
|
183
|
+
*
|
|
184
|
+
* Falls back to naming every child where `_debugOwner` is absent, which is the
|
|
185
|
+
* enclosure answer this replaced.
|
|
186
|
+
*
|
|
187
|
+
* Paths are not hashed. A path is an address that shifts when an unrelated
|
|
188
|
+
* sibling is inserted, so hashing one reports a change nobody made.
|
|
189
|
+
*/
|
|
190
|
+
export declare function shapeOf(boundary: Boundary, layout: boolean, rename?: Rename): Shape;
|
|
191
|
+
//# sourceMappingURL=boundary.d.ts.map
|