@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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { explainParting } from '../compare/explain.js';
|
|
2
|
+
import { boundarySnapshot } from '../compare/instance.js';
|
|
3
|
+
import { partingOf } from '../compare/parting.js';
|
|
4
|
+
import { movedBands } from './component-hash.js';
|
|
5
|
+
/**
|
|
6
|
+
* Every props class that produced more than one rendering *from one input*.
|
|
7
|
+
*
|
|
8
|
+
* Three refusals, and all three are the same refusal: a props digest is not a
|
|
9
|
+
* complete statement of a component's inputs, so most pairs of renderings that
|
|
10
|
+
* share one are not a contradiction. Measured on `examples/todomvc` before the
|
|
11
|
+
* checks below existed, **eleven divergences were reported and all eleven were
|
|
12
|
+
* false** — which is what a finding built on an incomplete key looks like.
|
|
13
|
+
*
|
|
14
|
+
* **Unknown props are not shared props.** Instances whose provenance did not
|
|
15
|
+
* survive are not known to have received the same thing. Grouping them and
|
|
16
|
+
* reporting that they render differently manufactures a finding out of missing
|
|
17
|
+
* data, in a system where absent must never read as equal.
|
|
18
|
+
*
|
|
19
|
+
* **Renderings that co-occur in one subject are not alternatives.** A component
|
|
20
|
+
* whose nodes are interrupted by a nested boundary is walked as two boundaries
|
|
21
|
+
* with one owner frame, so one `TextField` becomes a label-shaped rendering and
|
|
22
|
+
* an input-shaped one under a single props digest. That is one instance in two
|
|
23
|
+
* pieces, and it is indistinguishable from two instances that genuinely disagree
|
|
24
|
+
* — so it is not reported. A contradiction is a component that renders as A
|
|
25
|
+
* *here* and as B *there*, never both at once.
|
|
26
|
+
*
|
|
27
|
+
* **Different children are different inputs.** `propsDigest` excludes `children`
|
|
28
|
+
* deliberately (see `digestableProps` in `@variance-authority/react`: folding the
|
|
29
|
+
* subtree in would make every ancestor's props move on any descendant edit, and
|
|
30
|
+
* §6.2's root/collateral rule could never fire). The consequence is that
|
|
31
|
+
* `<Card><Stack/></Card>` and `<Card><Text/></Card>` share a props digest, and
|
|
32
|
+
* calling their different output a contradiction blames the component for its
|
|
33
|
+
* caller. Two proxies for "the children differed" are available and both are
|
|
34
|
+
* required to be quiet: the child components mounted, and the boundary's own
|
|
35
|
+
* text — which is where a string child lands.
|
|
36
|
+
*
|
|
37
|
+
* What survives is narrow on purpose. A movement wrongly dismissed as
|
|
38
|
+
* `contradicted` is an explanation nobody can act on; the same movement left
|
|
39
|
+
* unexplained lands on the suspect shortlist, where a second reading settles it.
|
|
40
|
+
* The asymmetry is the whole reason these checks are here rather than in prose.
|
|
41
|
+
*/
|
|
42
|
+
export function divergencesOf(components, snapshots) {
|
|
43
|
+
const divergences = [];
|
|
44
|
+
for (const entry of components) {
|
|
45
|
+
for (const group of entry.classes) {
|
|
46
|
+
if (group.props === undefined || group.renderings.length < 2)
|
|
47
|
+
continue;
|
|
48
|
+
if (!fromOneInput(group.renderings))
|
|
49
|
+
continue;
|
|
50
|
+
// Union against the first rather than over every pair. A band moves here
|
|
51
|
+
// when the renderings do not all agree on its digest, and a field that
|
|
52
|
+
// disagrees anywhere disagrees with the first somewhere — so the two are
|
|
53
|
+
// the same set, and the mapping stays in `movedBands` where the sensitivity
|
|
54
|
+
// tier reads it.
|
|
55
|
+
const [first, ...rest] = group.renderings;
|
|
56
|
+
const bands = new Set();
|
|
57
|
+
for (const other of rest)
|
|
58
|
+
for (const band of movedBands(first, other))
|
|
59
|
+
bands.add(band);
|
|
60
|
+
const partings = partingsOf(group.renderings, snapshots);
|
|
61
|
+
divergences.push({
|
|
62
|
+
component: entry.component,
|
|
63
|
+
props: group.props,
|
|
64
|
+
bands: [...bands],
|
|
65
|
+
renderings: group.renderings,
|
|
66
|
+
...(partings === undefined ? {} : { partings }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return divergences.sort((a, b) => byCodeUnit(a.component, b.component));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Read every rendering after the first against the first, for the moved input.
|
|
74
|
+
*
|
|
75
|
+
* Each site is lifted out of its subject and re-rooted at the component before
|
|
76
|
+
* the comparison — {@link boundarySnapshot}'s reason for existing. Comparing the
|
|
77
|
+
* two *subjects* instead would compare a receipt against a promo card, which is
|
|
78
|
+
* the difference the reader already knows about and not the one being asked.
|
|
79
|
+
*
|
|
80
|
+
* `undefined`, never `[]`, when no snapshot for the first rendering's subject was
|
|
81
|
+
* supplied: a run that was never handed the documents has not found the parting
|
|
82
|
+
* unexplainable, it has not looked.
|
|
83
|
+
*/
|
|
84
|
+
function partingsOf(renderings, snapshots) {
|
|
85
|
+
const first = liftFirstSite(renderings[0], snapshots);
|
|
86
|
+
if (first === undefined)
|
|
87
|
+
return undefined;
|
|
88
|
+
const found = [];
|
|
89
|
+
for (const [index, rendering] of renderings.entries()) {
|
|
90
|
+
if (index === 0)
|
|
91
|
+
continue;
|
|
92
|
+
const other = liftFirstSite(rendering, snapshots);
|
|
93
|
+
if (other === undefined)
|
|
94
|
+
continue;
|
|
95
|
+
found.push({ rendering: index, lines: explainParting(partingOf(first, other)) });
|
|
96
|
+
}
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* One rendering's first site, as a snapshot rooted at the component.
|
|
101
|
+
*
|
|
102
|
+
* The first site and not a chosen one: `sites` is ordered by subject then
|
|
103
|
+
* document position, so this is the same instance on every machine — which the
|
|
104
|
+
* report being a function of the plan requires.
|
|
105
|
+
*/
|
|
106
|
+
function liftFirstSite(rendering, snapshots) {
|
|
107
|
+
const site = rendering?.sites[0];
|
|
108
|
+
if (site === undefined)
|
|
109
|
+
return undefined;
|
|
110
|
+
const snapshot = snapshots.get(site.subject);
|
|
111
|
+
if (snapshot === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
return boundarySnapshot(snapshot, site.path);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Whether a props class' renderings can be said to have had the same inputs.
|
|
117
|
+
*
|
|
118
|
+
* The two checks the props digest cannot make for itself — see `divergencesOf`
|
|
119
|
+
* above for why each exists. Both are conservative in the same direction: they
|
|
120
|
+
* answer *no* whenever the run cannot tell, so what remains is a set of
|
|
121
|
+
* renderings that mounted the same children, said the same words, and still came
|
|
122
|
+
* out different, with no two of them observed in one subject.
|
|
123
|
+
*/
|
|
124
|
+
function fromOneInput(renderings) {
|
|
125
|
+
const [first, ...rest] = renderings;
|
|
126
|
+
if (first === undefined)
|
|
127
|
+
return false;
|
|
128
|
+
if (rest.some((other) => other.text !== first.text))
|
|
129
|
+
return false;
|
|
130
|
+
if (rest.some((other) => !sameOrder(other.renders, first.renders)))
|
|
131
|
+
return false;
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
for (const rendering of renderings) {
|
|
134
|
+
for (const subject of new Set(rendering.sites.map((site) => site.subject))) {
|
|
135
|
+
if (seen.has(subject))
|
|
136
|
+
return false;
|
|
137
|
+
seen.add(subject);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
function sameOrder(a, b) {
|
|
143
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Code-unit order, never `localeCompare`, for `composition.ts`' reason: this
|
|
147
|
+
* output reaches a report that is committed and read back on another runner.
|
|
148
|
+
*/
|
|
149
|
+
function byCodeUnit(a, b) {
|
|
150
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=divergence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"divergence.js","sourceRoot":"","sources":["../../src/attribute/divergence.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Band } from '../compare/band.js';\nimport { explainParting } from '../compare/explain.js';\nimport { boundarySnapshot } from '../compare/instance.js';\nimport { partingOf } from '../compare/parting.js';\nimport type { Digest } from '../format/hash.js';\nimport type { SemanticSnapshot } from '../format/snapshot.js';\nimport { movedBands } from './component-hash.js';\nimport type { ComponentEntry, Rendering } from './composition.js';\n\n/**\n * The finding with no baseline in it, and the sentence that makes it actionable.\n *\n * Split out of `composition.ts` because it is the only part of the graph that\n * reads *documents*. Everything else there folds digests a run already computed;\n * this lifts two instances out of the pages they were found in and compares them\n * — which is the only way to say `color`, and the difference between a reader\n * knowing which subject to open and knowing what is waiting there.\n */\n\n/**\n * The same component, the same inputs, and more than one rendering. At one commit.\n *\n * Not a regression and not a comparison — there is no baseline anywhere in it.\n * It is a statement that the component's own inputs do not determine its output,\n * which is either a fact about the design (a token, a theme, an ancestor) or the\n * reading not being repeatable. `bands` says which kind of difference it is, in\n * the same vocabulary a sensitivity absorbs, so a divergence entirely inside a\n * band the subject relaxes is one a reader can dismiss without opening it.\n *\n * *Inputs*, not *props*, and the difference is the whole finding: a props digest\n * excludes `children` and the checks in `divergencesOf` are what close the gap.\n * A suite can legitimately produce none of these — `examples/todomvc` produces\n * exactly zero — and that is the correct answer for a suite in which nothing\n * renders two ways from one input, not a section to be filled.\n */\nexport interface Divergence {\n readonly component: string;\n readonly props?: Digest;\n readonly bands: readonly Band[];\n /** At least two, sorted by how many sites each has, widest first. */\n readonly renderings: readonly Rendering[];\n\n /**\n * Why each rendering after the first parted from it.\n *\n * The half of this finding that used to be missing. \"`Price` rendered two ways\n * from one props digest\" states the contradiction and stops there, leaving the\n * reader to open two subjects and diff them by eye — which is the work the\n * component graph was built to remove. These lines name the input: an\n * ancestor's `color`, a context, a hook cell, or nothing readable at all.\n *\n * It is reachable here and nowhere else in the system, because a parting needs\n * both sides read the same way and this is the only comparison where that is\n * true by construction: both renderings come out of one run, off one collector,\n * at one commit. A parting across two revisions has to reconcile two configs\n * and two baselines first.\n *\n * Absent when the run supplied no snapshots to read — unknown, not \"nothing to\n * say\" (ADR-0002). Shorter than `renderings.length - 1` when some rendering's\n * site could not be lifted.\n */\n readonly partings?: readonly DivergenceParting[];\n}\n\n/** One rendering of a divergence, and why it parted from the first. */\nexport interface DivergenceParting {\n /** Index into {@link Divergence.renderings}, always at least 1. */\n readonly rendering: number;\n /** `explainParting` output, comparing that rendering against the first. */\n readonly lines: readonly string[];\n}\n\n/**\n * Every props class that produced more than one rendering *from one input*.\n *\n * Three refusals, and all three are the same refusal: a props digest is not a\n * complete statement of a component's inputs, so most pairs of renderings that\n * share one are not a contradiction. Measured on `examples/todomvc` before the\n * checks below existed, **eleven divergences were reported and all eleven were\n * false** — which is what a finding built on an incomplete key looks like.\n *\n * **Unknown props are not shared props.** Instances whose provenance did not\n * survive are not known to have received the same thing. Grouping them and\n * reporting that they render differently manufactures a finding out of missing\n * data, in a system where absent must never read as equal.\n *\n * **Renderings that co-occur in one subject are not alternatives.** A component\n * whose nodes are interrupted by a nested boundary is walked as two boundaries\n * with one owner frame, so one `TextField` becomes a label-shaped rendering and\n * an input-shaped one under a single props digest. That is one instance in two\n * pieces, and it is indistinguishable from two instances that genuinely disagree\n * — so it is not reported. A contradiction is a component that renders as A\n * *here* and as B *there*, never both at once.\n *\n * **Different children are different inputs.** `propsDigest` excludes `children`\n * deliberately (see `digestableProps` in `@variance-authority/react`: folding the\n * subtree in would make every ancestor's props move on any descendant edit, and\n * §6.2's root/collateral rule could never fire). The consequence is that\n * `<Card><Stack/></Card>` and `<Card><Text/></Card>` share a props digest, and\n * calling their different output a contradiction blames the component for its\n * caller. Two proxies for \"the children differed\" are available and both are\n * required to be quiet: the child components mounted, and the boundary's own\n * text — which is where a string child lands.\n *\n * What survives is narrow on purpose. A movement wrongly dismissed as\n * `contradicted` is an explanation nobody can act on; the same movement left\n * unexplained lands on the suspect shortlist, where a second reading settles it.\n * The asymmetry is the whole reason these checks are here rather than in prose.\n */\nexport function divergencesOf(\n components: readonly ComponentEntry[],\n snapshots: ReadonlyMap<string, SemanticSnapshot>,\n): readonly Divergence[] {\n const divergences: Divergence[] = [];\n\n for (const entry of components) {\n for (const group of entry.classes) {\n if (group.props === undefined || group.renderings.length < 2) continue;\n if (!fromOneInput(group.renderings)) continue;\n\n // Union against the first rather than over every pair. A band moves here\n // when the renderings do not all agree on its digest, and a field that\n // disagrees anywhere disagrees with the first somewhere — so the two are\n // the same set, and the mapping stays in `movedBands` where the sensitivity\n // tier reads it.\n const [first, ...rest] = group.renderings;\n const bands = new Set<Band>();\n for (const other of rest) for (const band of movedBands(first!, other)) bands.add(band);\n\n const partings = partingsOf(group.renderings, snapshots);\n\n divergences.push({\n component: entry.component,\n props: group.props,\n bands: [...bands],\n renderings: group.renderings,\n ...(partings === undefined ? {} : { partings }),\n });\n }\n }\n\n return divergences.sort((a, b) => byCodeUnit(a.component, b.component));\n}\n\n/**\n * Read every rendering after the first against the first, for the moved input.\n *\n * Each site is lifted out of its subject and re-rooted at the component before\n * the comparison — {@link boundarySnapshot}'s reason for existing. Comparing the\n * two *subjects* instead would compare a receipt against a promo card, which is\n * the difference the reader already knows about and not the one being asked.\n *\n * `undefined`, never `[]`, when no snapshot for the first rendering's subject was\n * supplied: a run that was never handed the documents has not found the parting\n * unexplainable, it has not looked.\n */\nfunction partingsOf(\n renderings: readonly Rendering[],\n snapshots: ReadonlyMap<string, SemanticSnapshot>,\n): readonly DivergenceParting[] | undefined {\n const first = liftFirstSite(renderings[0], snapshots);\n if (first === undefined) return undefined;\n\n const found: DivergenceParting[] = [];\n for (const [index, rendering] of renderings.entries()) {\n if (index === 0) continue;\n const other = liftFirstSite(rendering, snapshots);\n if (other === undefined) continue;\n found.push({ rendering: index, lines: explainParting(partingOf(first, other)) });\n }\n return found;\n}\n\n/**\n * One rendering's first site, as a snapshot rooted at the component.\n *\n * The first site and not a chosen one: `sites` is ordered by subject then\n * document position, so this is the same instance on every machine — which the\n * report being a function of the plan requires.\n */\nfunction liftFirstSite(\n rendering: Rendering | undefined,\n snapshots: ReadonlyMap<string, SemanticSnapshot>,\n): SemanticSnapshot | undefined {\n const site = rendering?.sites[0];\n if (site === undefined) return undefined;\n const snapshot = snapshots.get(site.subject);\n if (snapshot === undefined) return undefined;\n return boundarySnapshot(snapshot, site.path);\n}\n\n/**\n * Whether a props class' renderings can be said to have had the same inputs.\n *\n * The two checks the props digest cannot make for itself — see `divergencesOf`\n * above for why each exists. Both are conservative in the same direction: they\n * answer *no* whenever the run cannot tell, so what remains is a set of\n * renderings that mounted the same children, said the same words, and still came\n * out different, with no two of them observed in one subject.\n */\nfunction fromOneInput(renderings: readonly Rendering[]): boolean {\n const [first, ...rest] = renderings;\n if (first === undefined) return false;\n\n if (rest.some((other) => other.text !== first.text)) return false;\n if (rest.some((other) => !sameOrder(other.renders, first.renders))) return false;\n\n const seen = new Set<string>();\n for (const rendering of renderings) {\n for (const subject of new Set(rendering.sites.map((site) => site.subject))) {\n if (seen.has(subject)) return false;\n seen.add(subject);\n }\n }\n\n return true;\n}\n\nfunction sameOrder(a: readonly string[], b: readonly string[]): boolean {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n}\n\n/**\n * Code-unit order, never `localeCompare`, for `composition.ts`' reason: this\n * output reaches a report that is committed and read back on another runner.\n */\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@variance-authority/core/attribute` — from a position to a name to a file.
|
|
3
|
+
*
|
|
4
|
+
* The group that makes a difference actionable. Pixels cluster into regions,
|
|
5
|
+
* regions join the box tree, the tree knows which component produced each node,
|
|
6
|
+
* and the component resolves to a source location. The same machinery answers
|
|
7
|
+
* "which component is not holding still" and "which subjects cover this
|
|
8
|
+
* component".
|
|
9
|
+
*
|
|
10
|
+
* This is the answer to *"a pixel differ reports 1530 pixels changed"* — and the
|
|
11
|
+
* reason it is a group rather than a function is that every step of the journey
|
|
12
|
+
* is separately useful, and separately capable of refusing. `locateInstability`
|
|
13
|
+
* will not name a component for a difference that is beneath the semantic tier;
|
|
14
|
+
* `coverage` will not recommend deleting anything.
|
|
15
|
+
*/
|
|
16
|
+
export { locate } from './locate.js';
|
|
17
|
+
export type { Location, LocationStep } from './locate.js';
|
|
18
|
+
export { isolateRegions, subtractRegions, excludedBoxes } from './mask.js';
|
|
19
|
+
export type { ChangeMask, DiffRegion, ExcludedBox, Isolation, IsolationOptions, Subtraction, } from './mask.js';
|
|
20
|
+
export { attributeRegions, rankRegions } from './region.js';
|
|
21
|
+
export type { AttributedRegion, AttributionOptions, RankedRegion } from './region.js';
|
|
22
|
+
export { resolveSource, formatSource, indexSource, mergeSourceIndexes } from './source.js';
|
|
23
|
+
export type { SourceIndex, SourceRef, Resolution } from './source.js';
|
|
24
|
+
export { parseStackFrames, isVendorPath, writerLocationOf } from './stack.js';
|
|
25
|
+
export type { StackFrame } from './stack.js';
|
|
26
|
+
export { parseSourceMap, originalPositionFor, sourceMappingUrlOf, inlineSourceMapOf, } from './source-map.js';
|
|
27
|
+
export type { SourceMap, OriginalPosition } from './source-map.js';
|
|
28
|
+
export { createCallSiteResolver, locateProvenance, locateSites } from './call-site.js';
|
|
29
|
+
export type { CallSiteResolver, CallSiteStats, FetchModule, LocatableSite } from './call-site.js';
|
|
30
|
+
export { hashComponents, causesBetween, movedBands, movedBandsBetween, bandsBetween, UNATTRIBUTED, } from './component-hash.js';
|
|
31
|
+
export type { BandDigests, ComponentBands, ComponentHash } from './component-hash.js';
|
|
32
|
+
export { componentInstances, attributed } from './instances.js';
|
|
33
|
+
export type { ComponentInstance } from './instances.js';
|
|
34
|
+
export { composeSubjects } from './composition.js';
|
|
35
|
+
export type { Composition, ComponentEntry, Echo, PropsClass, Rendering, Site, SubjectComposition, } from './composition.js';
|
|
36
|
+
export type { Divergence, DivergenceParting } from './divergence.js';
|
|
37
|
+
export { attributeMovement } from './movement.js';
|
|
38
|
+
export type { Attribution, Cause, Evidence, Moved, Movement } from './movement.js';
|
|
39
|
+
export { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';
|
|
40
|
+
export type { Coverage, ComponentCoverage, SubjectCoverage, SubjectValue } from './coverage.js';
|
|
41
|
+
export { locateInstability, summarizeInstability } from './instability.js';
|
|
42
|
+
export type { Instability, InstabilityBand, UnstableLocation, UnstableProperty, } from './instability.js';
|
|
43
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@variance-authority/core/attribute` — from a position to a name to a file.
|
|
3
|
+
*
|
|
4
|
+
* The group that makes a difference actionable. Pixels cluster into regions,
|
|
5
|
+
* regions join the box tree, the tree knows which component produced each node,
|
|
6
|
+
* and the component resolves to a source location. The same machinery answers
|
|
7
|
+
* "which component is not holding still" and "which subjects cover this
|
|
8
|
+
* component".
|
|
9
|
+
*
|
|
10
|
+
* This is the answer to *"a pixel differ reports 1530 pixels changed"* — and the
|
|
11
|
+
* reason it is a group rather than a function is that every step of the journey
|
|
12
|
+
* is separately useful, and separately capable of refusing. `locateInstability`
|
|
13
|
+
* will not name a component for a difference that is beneath the semantic tier;
|
|
14
|
+
* `coverage` will not recommend deleting anything.
|
|
15
|
+
*/
|
|
16
|
+
export { locate } from './locate.js';
|
|
17
|
+
export { isolateRegions, subtractRegions, excludedBoxes } from './mask.js';
|
|
18
|
+
export { attributeRegions, rankRegions } from './region.js';
|
|
19
|
+
export { resolveSource, formatSource, indexSource, mergeSourceIndexes } from './source.js';
|
|
20
|
+
export { parseStackFrames, isVendorPath, writerLocationOf } from './stack.js';
|
|
21
|
+
export { parseSourceMap, originalPositionFor, sourceMappingUrlOf, inlineSourceMapOf, } from './source-map.js';
|
|
22
|
+
export { createCallSiteResolver, locateProvenance, locateSites } from './call-site.js';
|
|
23
|
+
export { hashComponents, causesBetween, movedBands, movedBandsBetween, bandsBetween, UNATTRIBUTED, } from './component-hash.js';
|
|
24
|
+
export { componentInstances, attributed } from './instances.js';
|
|
25
|
+
export { composeSubjects } from './composition.js';
|
|
26
|
+
export { attributeMovement } from './movement.js';
|
|
27
|
+
export { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';
|
|
28
|
+
export { locateInstability, summarizeInstability } from './instability.js';
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/attribute/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAU3E,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAG3F,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9E,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGvF,OAAO,EACL,cAAc,EACd,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,GACb,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGhE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAanD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAG5E,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * `@variance-authority/core/attribute` — from a position to a name to a file.\n *\n * The group that makes a difference actionable. Pixels cluster into regions,\n * regions join the box tree, the tree knows which component produced each node,\n * and the component resolves to a source location. The same machinery answers\n * \"which component is not holding still\" and \"which subjects cover this\n * component\".\n *\n * This is the answer to *\"a pixel differ reports 1530 pixels changed\"* — and the\n * reason it is a group rather than a function is that every step of the journey\n * is separately useful, and separately capable of refusing. `locateInstability`\n * will not name a component for a difference that is beneath the semantic tier;\n * `coverage` will not recommend deleting anything.\n */\n\nexport { locate } from './locate.js';\nexport type { Location, LocationStep } from './locate.js';\n\nexport { isolateRegions, subtractRegions, excludedBoxes } from './mask.js';\nexport type {\n ChangeMask,\n DiffRegion,\n ExcludedBox,\n Isolation,\n IsolationOptions,\n Subtraction,\n} from './mask.js';\n\nexport { attributeRegions, rankRegions } from './region.js';\nexport type { AttributedRegion, AttributionOptions, RankedRegion } from './region.js';\n\nexport { resolveSource, formatSource, indexSource, mergeSourceIndexes } from './source.js';\nexport type { SourceIndex, SourceRef, Resolution } from './source.js';\n\nexport { parseStackFrames, isVendorPath, writerLocationOf } from './stack.js';\nexport type { StackFrame } from './stack.js';\n\nexport {\n parseSourceMap,\n originalPositionFor,\n sourceMappingUrlOf,\n inlineSourceMapOf,\n} from './source-map.js';\nexport type { SourceMap, OriginalPosition } from './source-map.js';\n\nexport { createCallSiteResolver, locateProvenance, locateSites } from './call-site.js';\nexport type { CallSiteResolver, CallSiteStats, FetchModule, LocatableSite } from './call-site.js';\n\nexport {\n hashComponents,\n causesBetween,\n movedBands,\n movedBandsBetween,\n bandsBetween,\n UNATTRIBUTED,\n} from './component-hash.js';\nexport type { BandDigests, ComponentBands, ComponentHash } from './component-hash.js';\n\nexport { componentInstances, attributed } from './instances.js';\nexport type { ComponentInstance } from './instances.js';\n\nexport { composeSubjects } from './composition.js';\nexport type {\n Composition,\n ComponentEntry,\n Echo,\n PropsClass,\n Rendering,\n Site,\n SubjectComposition,\n} from './composition.js';\n\nexport type { Divergence, DivergenceParting } from './divergence.js';\n\nexport { attributeMovement } from './movement.js';\nexport type { Attribution, Cause, Evidence, Moved, Movement } from './movement.js';\n\nexport { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';\nexport type { Coverage, ComponentCoverage, SubjectCoverage, SubjectValue } from './coverage.js';\n\nexport { locateInstability, summarizeInstability } from './instability.js';\nexport type {\n Instability,\n InstabilityBand,\n UnstableLocation,\n UnstableProperty,\n} from './instability.js';\n"]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type Band } from '../compare/band.js';
|
|
2
|
+
import { type SourceIndex } from './source.js';
|
|
3
|
+
import type { NodePath, SemanticSnapshot } from '../format/snapshot.js';
|
|
4
|
+
/**
|
|
5
|
+
* Where an instability lives, and what kind it is.
|
|
6
|
+
*
|
|
7
|
+
* Two captures of the *same* subject at the *same* commit are supposed to agree.
|
|
8
|
+
* When they do not, the usual response is to capture again until two agree and
|
|
9
|
+
* proceed — which works, and destroys the only evidence that existed. The
|
|
10
|
+
* disagreement was the finding. A run that resolves it by repetition has paid
|
|
11
|
+
* for the extra captures and learned nothing, and will pay again tomorrow.
|
|
12
|
+
*
|
|
13
|
+
* There is always a cause and always a place. A pixel comparison cannot supply
|
|
14
|
+
* either: it reports that some pixels moved, which is true of every cause
|
|
15
|
+
* equally. The semantic representation can, because the same two captures also
|
|
16
|
+
* carry structure, resolved declarations and geometry — so the question "what
|
|
17
|
+
* moved between two observations of one commit" has an answer in terms of a
|
|
18
|
+
* component, a declaration, and a file.
|
|
19
|
+
*
|
|
20
|
+
* That answer is what makes prevention possible instead of suppression. An
|
|
21
|
+
* animation caught mid-flight is fixed by pausing it, not by masking the region
|
|
22
|
+
* it happens to occupy this week; a clock is fixed at the fixture. Both fixes are
|
|
23
|
+
* at a source location, and both make every later run cheaper — the stabilisation
|
|
24
|
+
* captures stop being needed at all.
|
|
25
|
+
*
|
|
26
|
+
* The honest boundary: when nothing moved semantically and pixels still differ,
|
|
27
|
+
* the cause is below the box tree and **no component is responsible**. Naming one
|
|
28
|
+
* would be inventing a location. That case is reported as its own kind, because
|
|
29
|
+
* "we cannot see it from here" and "nothing is wrong" are different sentences.
|
|
30
|
+
*/
|
|
31
|
+
export type InstabilityBand = Band
|
|
32
|
+
/** Pixels moved and the box tree did not. Rasterization, compositing, fonts. */
|
|
33
|
+
| 'sub-semantic' | 'none';
|
|
34
|
+
export interface UnstableProperty {
|
|
35
|
+
readonly property: string;
|
|
36
|
+
readonly from?: string | undefined;
|
|
37
|
+
readonly to?: string | undefined;
|
|
38
|
+
}
|
|
39
|
+
export interface UnstableLocation {
|
|
40
|
+
readonly component: string;
|
|
41
|
+
readonly bands: readonly Band[];
|
|
42
|
+
readonly properties: readonly UnstableProperty[];
|
|
43
|
+
/** Landmark phrase, e.g. `main → list item 2 of 3`. */
|
|
44
|
+
readonly where?: string;
|
|
45
|
+
readonly path?: NodePath;
|
|
46
|
+
/** Custom property the moving value resolved through, when there is one. */
|
|
47
|
+
readonly token?: string;
|
|
48
|
+
readonly deltas: number;
|
|
49
|
+
/**
|
|
50
|
+
* A conservative reading of the *shape* of the evidence, not a conclusion.
|
|
51
|
+
*
|
|
52
|
+
* Present only where one shape fits and the others do not. Absent is the
|
|
53
|
+
* common case and is not a failure — the location and the moved declarations
|
|
54
|
+
* are the deliverable; this is a hint about where to look first.
|
|
55
|
+
*/
|
|
56
|
+
readonly likely?: 'animation' | 'dynamic-content' | 'displacement';
|
|
57
|
+
}
|
|
58
|
+
export interface Instability {
|
|
59
|
+
readonly stable: boolean;
|
|
60
|
+
readonly band: InstabilityBand;
|
|
61
|
+
/** Ordered by how many deltas landed in each, widest first. */
|
|
62
|
+
readonly locations: readonly UnstableLocation[];
|
|
63
|
+
/** One sentence stating what moved and what that implies. */
|
|
64
|
+
readonly because: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Compare two observations of one subject that ought to be identical.
|
|
68
|
+
*
|
|
69
|
+
* `pixelsDiffer` is supplied by the caller when an image comparison was also
|
|
70
|
+
* taken. It is what separates "stable" from "unstable in a way this tier cannot
|
|
71
|
+
* see", and omitting it means the second answer is unavailable rather than
|
|
72
|
+
* assumed — a subject reported stable on the strength of a comparison nobody
|
|
73
|
+
* made would be the same false negative this system exists to refuse.
|
|
74
|
+
*/
|
|
75
|
+
export declare function locateInstability(before: SemanticSnapshot, after: SemanticSnapshot, options?: {
|
|
76
|
+
readonly pixelsDiffer?: boolean;
|
|
77
|
+
}): Instability;
|
|
78
|
+
/**
|
|
79
|
+
* The instability as the thing a person reads, with a file on the end.
|
|
80
|
+
*
|
|
81
|
+
* Same shape as every other report here: a cause per line, and a path an editor
|
|
82
|
+
* opens, because `Spinner` is an identifier and `src/ds/components.tsx:88` is an
|
|
83
|
+
* edit.
|
|
84
|
+
*/
|
|
85
|
+
export declare function summarizeInstability(instability: Instability, options?: {
|
|
86
|
+
readonly source?: SourceIndex;
|
|
87
|
+
readonly limit?: number;
|
|
88
|
+
}): string;
|
|
89
|
+
//# sourceMappingURL=instability.d.ts.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { loudestBand } from '../compare/band.js';
|
|
2
|
+
import { diffSnapshots } from '../compare/diff/index.js';
|
|
3
|
+
import { formatSource, resolveSource } from './source.js';
|
|
4
|
+
/** Properties whose movement is characteristic of something still animating. */
|
|
5
|
+
const ANIMATED = new Set(['transform', 'opacity', 'filter', 'offset-distance', 'rotate', 'scale', 'translate']);
|
|
6
|
+
/**
|
|
7
|
+
* Compare two observations of one subject that ought to be identical.
|
|
8
|
+
*
|
|
9
|
+
* `pixelsDiffer` is supplied by the caller when an image comparison was also
|
|
10
|
+
* taken. It is what separates "stable" from "unstable in a way this tier cannot
|
|
11
|
+
* see", and omitting it means the second answer is unavailable rather than
|
|
12
|
+
* assumed — a subject reported stable on the strength of a comparison nobody
|
|
13
|
+
* made would be the same false negative this system exists to refuse.
|
|
14
|
+
*/
|
|
15
|
+
export function locateInstability(before, after, options = {}) {
|
|
16
|
+
// `diffSnapshots` refuses a cross-subject or cross-profile comparison, which
|
|
17
|
+
// is the right failure here: two captures that are not of the same thing
|
|
18
|
+
// cannot be evidence about stability.
|
|
19
|
+
const diff = diffSnapshots(before, after);
|
|
20
|
+
if (diff.deltas.length === 0) {
|
|
21
|
+
if (options.pixelsDiffer === true) {
|
|
22
|
+
return {
|
|
23
|
+
stable: false,
|
|
24
|
+
band: 'sub-semantic',
|
|
25
|
+
locations: [],
|
|
26
|
+
because: 'two captures of the same subject are identical in structure, resolved style and ' +
|
|
27
|
+
'geometry, and their images are not. The cause is below the box tree — glyph ' +
|
|
28
|
+
'rasterization, compositing, or a font resolving differently — so no component is ' +
|
|
29
|
+
'responsible and there is no source location to fix. This belongs to the ' +
|
|
30
|
+
'environment key, not to the code',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
stable: true,
|
|
35
|
+
band: 'none',
|
|
36
|
+
locations: [],
|
|
37
|
+
because: options.pixelsDiffer === false
|
|
38
|
+
? 'two captures of the same subject agree in structure, style, geometry and pixels'
|
|
39
|
+
: 'two captures of the same subject agree in structure, style and geometry; no image ' +
|
|
40
|
+
'comparison was supplied, so sub-semantic movement is unobserved rather than absent',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const locations = group(diff.deltas);
|
|
44
|
+
return {
|
|
45
|
+
stable: false,
|
|
46
|
+
band: worst(diff.deltas.map((delta) => delta.band)),
|
|
47
|
+
locations,
|
|
48
|
+
because: sentence(locations, diff.deltas.length),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The instability as the thing a person reads, with a file on the end.
|
|
53
|
+
*
|
|
54
|
+
* Same shape as every other report here: a cause per line, and a path an editor
|
|
55
|
+
* opens, because `Spinner` is an identifier and `src/ds/components.tsx:88` is an
|
|
56
|
+
* edit.
|
|
57
|
+
*/
|
|
58
|
+
export function summarizeInstability(instability, options = {}) {
|
|
59
|
+
if (instability.stable)
|
|
60
|
+
return instability.because;
|
|
61
|
+
const limit = options.limit ?? 5;
|
|
62
|
+
const shown = instability.locations.slice(0, limit);
|
|
63
|
+
const lines = shown.map((location) => {
|
|
64
|
+
const file = options.source === undefined ? null : resolveSource(location.component, options.source);
|
|
65
|
+
const moved = location.properties
|
|
66
|
+
.slice(0, 3)
|
|
67
|
+
.map((property) => property.from === undefined && property.to === undefined
|
|
68
|
+
? property.property
|
|
69
|
+
: `${property.property} ${property.from ?? '(absent)'} → ${property.to ?? '(absent)'}`);
|
|
70
|
+
return [
|
|
71
|
+
` ${location.component} — ${location.deltas} delta(s) in ${location.bands.join(', ')}` +
|
|
72
|
+
(location.likely !== undefined ? ` [likely ${location.likely}]` : ''),
|
|
73
|
+
...moved.map((line) => ` ${line}`),
|
|
74
|
+
location.properties.length > 3
|
|
75
|
+
? ` +${location.properties.length - 3} more propert(y|ies) not listed`
|
|
76
|
+
: null,
|
|
77
|
+
location.where !== undefined ? ` in ${location.where}` : null,
|
|
78
|
+
location.token !== undefined ? ` through ${location.token}` : null,
|
|
79
|
+
file !== null ? ` ${formatSource(file)}` : null,
|
|
80
|
+
]
|
|
81
|
+
.filter((line) => line !== null)
|
|
82
|
+
.join('\n');
|
|
83
|
+
});
|
|
84
|
+
return [
|
|
85
|
+
`[unstable] ${instability.because}`,
|
|
86
|
+
'',
|
|
87
|
+
...lines,
|
|
88
|
+
...(instability.locations.length > shown.length
|
|
89
|
+
? [` +${instability.locations.length - shown.length} more location(s) not listed`]
|
|
90
|
+
: []),
|
|
91
|
+
].join('\n');
|
|
92
|
+
}
|
|
93
|
+
function group(deltas) {
|
|
94
|
+
const byComponent = new Map();
|
|
95
|
+
for (const delta of deltas) {
|
|
96
|
+
// `createdBy` names the component whose JSX produced the node; `owners[0]`
|
|
97
|
+
// names the nearest enclosing one. Prefer the author, because a fix is made
|
|
98
|
+
// where the markup is written, not where it ends up.
|
|
99
|
+
const component = delta.createdBy ?? delta.owners?.[0]?.name ?? '(unattributed)';
|
|
100
|
+
const entry = byComponent.get(component) ?? {
|
|
101
|
+
bands: new Set(),
|
|
102
|
+
properties: new Map(),
|
|
103
|
+
kinds: new Set(),
|
|
104
|
+
count: 0,
|
|
105
|
+
};
|
|
106
|
+
entry.bands.add(delta.band);
|
|
107
|
+
entry.kinds.add(delta.kind);
|
|
108
|
+
entry.count += 1;
|
|
109
|
+
// A whole-node delta — text, a role, a node appearing — carries no
|
|
110
|
+
// `property`. Reading its absence as "only geometry moved" would file a
|
|
111
|
+
// changing clock under displacement, so the kind is what is consulted and
|
|
112
|
+
// the property list is only ever additive.
|
|
113
|
+
if (delta.property !== undefined && !entry.properties.has(delta.property)) {
|
|
114
|
+
entry.properties.set(delta.property, {
|
|
115
|
+
property: delta.property,
|
|
116
|
+
from: delta.from,
|
|
117
|
+
to: delta.to,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (entry.where === undefined && delta.where !== undefined)
|
|
121
|
+
entry.where = delta.where;
|
|
122
|
+
if (entry.path === undefined)
|
|
123
|
+
entry.path = delta.path;
|
|
124
|
+
if (entry.token === undefined && delta.token !== undefined)
|
|
125
|
+
entry.token = delta.token;
|
|
126
|
+
byComponent.set(component, entry);
|
|
127
|
+
}
|
|
128
|
+
return [...byComponent.entries()]
|
|
129
|
+
.map(([component, entry]) => {
|
|
130
|
+
const properties = [...entry.properties.values()];
|
|
131
|
+
return {
|
|
132
|
+
component,
|
|
133
|
+
bands: [...entry.bands],
|
|
134
|
+
properties,
|
|
135
|
+
...(entry.where !== undefined ? { where: entry.where } : {}),
|
|
136
|
+
...(entry.path !== undefined ? { path: entry.path } : {}),
|
|
137
|
+
...(entry.token !== undefined ? { token: entry.token } : {}),
|
|
138
|
+
deltas: entry.count,
|
|
139
|
+
...(guess(properties, entry.kinds) !== undefined
|
|
140
|
+
? { likely: guess(properties, entry.kinds) }
|
|
141
|
+
: {}),
|
|
142
|
+
};
|
|
143
|
+
})
|
|
144
|
+
.sort((a, b) => b.deltas - a.deltas ||
|
|
145
|
+
(a.component < b.component ? -1 : a.component > b.component ? 1 : 0));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Read the shape of the evidence, and only where one reading fits.
|
|
149
|
+
*
|
|
150
|
+
* Deliberately narrow. A guess that fires on ambiguous evidence is worse than no
|
|
151
|
+
* guess: it sends someone to the wrong file with confidence, and the location
|
|
152
|
+
* and the moved declarations — which are facts — get read as if they carried the
|
|
153
|
+
* same certainty as the label.
|
|
154
|
+
*/
|
|
155
|
+
function guess(properties, kinds) {
|
|
156
|
+
// Content that rewrote itself between two captures of one commit. Checked
|
|
157
|
+
// before the others because a changing string also moves the box around it,
|
|
158
|
+
// and the text is the cause while the movement is its consequence.
|
|
159
|
+
if (kinds.has('text-changed'))
|
|
160
|
+
return 'dynamic-content';
|
|
161
|
+
if (properties.length === 0) {
|
|
162
|
+
// Geometry moved and this component declared nothing at all. Something
|
|
163
|
+
// upstream reflowed it, so the cause is elsewhere and this is only where it
|
|
164
|
+
// landed — which is worth saying, because it is the one reading that points
|
|
165
|
+
// away from the component being named.
|
|
166
|
+
const onlyGeometry = [...kinds].every((kind) => kind === 'rect-changed' || kind === 'node-moved');
|
|
167
|
+
return onlyGeometry ? 'displacement' : undefined;
|
|
168
|
+
}
|
|
169
|
+
if (properties.every((property) => ANIMATED.has(property.property)))
|
|
170
|
+
return 'animation';
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
function worst(bands) {
|
|
174
|
+
return loudestBand(bands) ?? 'none';
|
|
175
|
+
}
|
|
176
|
+
function sentence(locations, deltas) {
|
|
177
|
+
const named = locations.slice(0, 3).map((location) => location.component);
|
|
178
|
+
const rest = locations.length > named.length ? ` (+${locations.length - named.length} more)` : '';
|
|
179
|
+
return (`two captures of the same subject at the same commit disagree: ${deltas} delta(s) ` +
|
|
180
|
+
`across ${locations.length} component(s) — ${named.join(', ')}${rest}. ` +
|
|
181
|
+
'This is not noise to retry away; the location below is where it is caused');
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=instability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instability.js","sourceRoot":"","sources":["../../src/attribute/instability.ts"],"names":[],"mappings":"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","sourcesContent":["import { loudestBand, type Band, type DeltaKind } from '../compare/band.js';\nimport type { Delta } from '../compare/diff/delta.js';\nimport { diffSnapshots } from '../compare/diff/index.js';\nimport { formatSource, resolveSource, type SourceIndex } from './source.js';\nimport type { NodePath, SemanticSnapshot } from '../format/snapshot.js';\n\n/**\n * Where an instability lives, and what kind it is.\n *\n * Two captures of the *same* subject at the *same* commit are supposed to agree.\n * When they do not, the usual response is to capture again until two agree and\n * proceed — which works, and destroys the only evidence that existed. The\n * disagreement was the finding. A run that resolves it by repetition has paid\n * for the extra captures and learned nothing, and will pay again tomorrow.\n *\n * There is always a cause and always a place. A pixel comparison cannot supply\n * either: it reports that some pixels moved, which is true of every cause\n * equally. The semantic representation can, because the same two captures also\n * carry structure, resolved declarations and geometry — so the question \"what\n * moved between two observations of one commit\" has an answer in terms of a\n * component, a declaration, and a file.\n *\n * That answer is what makes prevention possible instead of suppression. An\n * animation caught mid-flight is fixed by pausing it, not by masking the region\n * it happens to occupy this week; a clock is fixed at the fixture. Both fixes are\n * at a source location, and both make every later run cheaper — the stabilisation\n * captures stop being needed at all.\n *\n * The honest boundary: when nothing moved semantically and pixels still differ,\n * the cause is below the box tree and **no component is responsible**. Naming one\n * would be inventing a location. That case is reported as its own kind, because\n * \"we cannot see it from here\" and \"nothing is wrong\" are different sentences.\n */\n\nexport type InstabilityBand =\n | Band\n /** Pixels moved and the box tree did not. Rasterization, compositing, fonts. */\n | 'sub-semantic'\n | 'none';\n\nexport interface UnstableProperty {\n readonly property: string;\n readonly from?: string | undefined;\n readonly to?: string | undefined;\n}\n\nexport interface UnstableLocation {\n readonly component: string;\n readonly bands: readonly Band[];\n readonly properties: readonly UnstableProperty[];\n\n /** Landmark phrase, e.g. `main → list item 2 of 3`. */\n readonly where?: string;\n readonly path?: NodePath;\n /** Custom property the moving value resolved through, when there is one. */\n readonly token?: string;\n\n readonly deltas: number;\n\n /**\n * A conservative reading of the *shape* of the evidence, not a conclusion.\n *\n * Present only where one shape fits and the others do not. Absent is the\n * common case and is not a failure — the location and the moved declarations\n * are the deliverable; this is a hint about where to look first.\n */\n readonly likely?: 'animation' | 'dynamic-content' | 'displacement';\n}\n\nexport interface Instability {\n readonly stable: boolean;\n readonly band: InstabilityBand;\n /** Ordered by how many deltas landed in each, widest first. */\n readonly locations: readonly UnstableLocation[];\n /** One sentence stating what moved and what that implies. */\n readonly because: string;\n}\n\n/** Properties whose movement is characteristic of something still animating. */\nconst ANIMATED = new Set(['transform', 'opacity', 'filter', 'offset-distance', 'rotate', 'scale', 'translate']);\n\n/**\n * Compare two observations of one subject that ought to be identical.\n *\n * `pixelsDiffer` is supplied by the caller when an image comparison was also\n * taken. It is what separates \"stable\" from \"unstable in a way this tier cannot\n * see\", and omitting it means the second answer is unavailable rather than\n * assumed — a subject reported stable on the strength of a comparison nobody\n * made would be the same false negative this system exists to refuse.\n */\nexport function locateInstability(\n before: SemanticSnapshot,\n after: SemanticSnapshot,\n options: { readonly pixelsDiffer?: boolean } = {},\n): Instability {\n // `diffSnapshots` refuses a cross-subject or cross-profile comparison, which\n // is the right failure here: two captures that are not of the same thing\n // cannot be evidence about stability.\n const diff = diffSnapshots(before, after);\n\n if (diff.deltas.length === 0) {\n if (options.pixelsDiffer === true) {\n return {\n stable: false,\n band: 'sub-semantic',\n locations: [],\n because:\n 'two captures of the same subject are identical in structure, resolved style and ' +\n 'geometry, and their images are not. The cause is below the box tree — glyph ' +\n 'rasterization, compositing, or a font resolving differently — so no component is ' +\n 'responsible and there is no source location to fix. This belongs to the ' +\n 'environment key, not to the code',\n };\n }\n\n return {\n stable: true,\n band: 'none',\n locations: [],\n because:\n options.pixelsDiffer === false\n ? 'two captures of the same subject agree in structure, style, geometry and pixels'\n : 'two captures of the same subject agree in structure, style and geometry; no image ' +\n 'comparison was supplied, so sub-semantic movement is unobserved rather than absent',\n };\n }\n\n const locations = group(diff.deltas);\n\n return {\n stable: false,\n band: worst(diff.deltas.map((delta) => delta.band)),\n locations,\n because: sentence(locations, diff.deltas.length),\n };\n}\n\n/**\n * The instability as the thing a person reads, with a file on the end.\n *\n * Same shape as every other report here: a cause per line, and a path an editor\n * opens, because `Spinner` is an identifier and `src/ds/components.tsx:88` is an\n * edit.\n */\nexport function summarizeInstability(\n instability: Instability,\n options: { readonly source?: SourceIndex; readonly limit?: number } = {},\n): string {\n if (instability.stable) return instability.because;\n\n const limit = options.limit ?? 5;\n const shown = instability.locations.slice(0, limit);\n\n const lines = shown.map((location) => {\n const file =\n options.source === undefined ? null : resolveSource(location.component, options.source);\n\n const moved = location.properties\n .slice(0, 3)\n .map((property) =>\n property.from === undefined && property.to === undefined\n ? property.property\n : `${property.property} ${property.from ?? '(absent)'} → ${property.to ?? '(absent)'}`,\n );\n\n return [\n ` ${location.component} — ${location.deltas} delta(s) in ${location.bands.join(', ')}` +\n (location.likely !== undefined ? ` [likely ${location.likely}]` : ''),\n ...moved.map((line) => ` ${line}`),\n location.properties.length > 3\n ? ` +${location.properties.length - 3} more propert(y|ies) not listed`\n : null,\n location.where !== undefined ? ` in ${location.where}` : null,\n location.token !== undefined ? ` through ${location.token}` : null,\n file !== null ? ` ${formatSource(file)}` : null,\n ]\n .filter((line): line is string => line !== null)\n .join('\\n');\n });\n\n return [\n `[unstable] ${instability.because}`,\n '',\n ...lines,\n ...(instability.locations.length > shown.length\n ? [` +${instability.locations.length - shown.length} more location(s) not listed`]\n : []),\n ].join('\\n');\n}\n\nfunction group(deltas: readonly Delta[]): readonly UnstableLocation[] {\n const byComponent = new Map<\n string,\n {\n bands: Set<Band>;\n properties: Map<string, UnstableProperty>;\n kinds: Set<DeltaKind>;\n where?: string;\n path?: NodePath;\n token?: string;\n count: number;\n }\n >();\n\n for (const delta of deltas) {\n // `createdBy` names the component whose JSX produced the node; `owners[0]`\n // names the nearest enclosing one. Prefer the author, because a fix is made\n // where the markup is written, not where it ends up.\n const component = delta.createdBy ?? delta.owners?.[0]?.name ?? '(unattributed)';\n\n const entry = byComponent.get(component) ?? {\n bands: new Set<Band>(),\n properties: new Map<string, UnstableProperty>(),\n kinds: new Set<DeltaKind>(),\n count: 0,\n };\n\n entry.bands.add(delta.band);\n entry.kinds.add(delta.kind);\n entry.count += 1;\n\n // A whole-node delta — text, a role, a node appearing — carries no\n // `property`. Reading its absence as \"only geometry moved\" would file a\n // changing clock under displacement, so the kind is what is consulted and\n // the property list is only ever additive.\n if (delta.property !== undefined && !entry.properties.has(delta.property)) {\n entry.properties.set(delta.property, {\n property: delta.property,\n from: delta.from,\n to: delta.to,\n });\n }\n\n if (entry.where === undefined && delta.where !== undefined) entry.where = delta.where;\n if (entry.path === undefined) entry.path = delta.path;\n if (entry.token === undefined && delta.token !== undefined) entry.token = delta.token;\n\n byComponent.set(component, entry);\n }\n\n return [...byComponent.entries()]\n .map(([component, entry]) => {\n const properties = [...entry.properties.values()];\n return {\n component,\n bands: [...entry.bands],\n properties,\n ...(entry.where !== undefined ? { where: entry.where } : {}),\n ...(entry.path !== undefined ? { path: entry.path } : {}),\n ...(entry.token !== undefined ? { token: entry.token } : {}),\n deltas: entry.count,\n ...(guess(properties, entry.kinds) !== undefined\n ? { likely: guess(properties, entry.kinds)! }\n : {}),\n };\n })\n .sort(\n (a, b) =>\n b.deltas - a.deltas ||\n (a.component < b.component ? -1 : a.component > b.component ? 1 : 0),\n );\n}\n\n/**\n * Read the shape of the evidence, and only where one reading fits.\n *\n * Deliberately narrow. A guess that fires on ambiguous evidence is worse than no\n * guess: it sends someone to the wrong file with confidence, and the location\n * and the moved declarations — which are facts — get read as if they carried the\n * same certainty as the label.\n */\nfunction guess(\n properties: readonly UnstableProperty[],\n kinds: ReadonlySet<DeltaKind>,\n): UnstableLocation['likely'] | undefined {\n // Content that rewrote itself between two captures of one commit. Checked\n // before the others because a changing string also moves the box around it,\n // and the text is the cause while the movement is its consequence.\n if (kinds.has('text-changed')) return 'dynamic-content';\n\n if (properties.length === 0) {\n // Geometry moved and this component declared nothing at all. Something\n // upstream reflowed it, so the cause is elsewhere and this is only where it\n // landed — which is worth saying, because it is the one reading that points\n // away from the component being named.\n const onlyGeometry = [...kinds].every((kind) => kind === 'rect-changed' || kind === 'node-moved');\n return onlyGeometry ? 'displacement' : undefined;\n }\n\n if (properties.every((property) => ANIMATED.has(property.property))) return 'animation';\n\n return undefined;\n}\n\nfunction worst(bands: readonly Band[]): InstabilityBand {\n return loudestBand(bands) ?? 'none';\n}\n\nfunction sentence(locations: readonly UnstableLocation[], deltas: number): string {\n const named = locations.slice(0, 3).map((location) => location.component);\n const rest = locations.length > named.length ? ` (+${locations.length - named.length} more)` : '';\n\n return (\n `two captures of the same subject at the same commit disagree: ${deltas} delta(s) ` +\n `across ${locations.length} component(s) — ${named.join(', ')}${rest}. ` +\n 'This is not noise to retry away; the location below is where it is caused'\n );\n}\n"]}
|