@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,169 @@
|
|
|
1
|
+
import { divergencesOf } from './divergence.js';
|
|
2
|
+
import { attributed } from './instances.js';
|
|
3
|
+
/**
|
|
4
|
+
* Fold a run's per-subject instance lists into the graph.
|
|
5
|
+
*
|
|
6
|
+
* Pure, ordered, and a function of its input alone — the run's report has to be
|
|
7
|
+
* a function of the plan, and a phase that accumulated as a worker pool finished
|
|
8
|
+
* would produce a different artefact from the same suite on a slower machine.
|
|
9
|
+
*/
|
|
10
|
+
export function composeSubjects(subjects) {
|
|
11
|
+
const entries = new Map();
|
|
12
|
+
const order = subjects.map((subject) => subject.subject);
|
|
13
|
+
for (const subject of subjects) {
|
|
14
|
+
const example = exampleOf(subject.instances);
|
|
15
|
+
for (const instance of subject.instances) {
|
|
16
|
+
if (!attributed(instance))
|
|
17
|
+
continue;
|
|
18
|
+
const entry = accumulate(entries, instance.component);
|
|
19
|
+
if (entry.subjects.at(-1) !== subject.subject)
|
|
20
|
+
entry.subjects.push(subject.subject);
|
|
21
|
+
entry.instances += 1;
|
|
22
|
+
if (example === instance.component && entry.examples.at(-1) !== subject.subject) {
|
|
23
|
+
entry.examples.push(subject.subject);
|
|
24
|
+
}
|
|
25
|
+
if (instance.within !== undefined)
|
|
26
|
+
entry.within.add(instance.within);
|
|
27
|
+
if (instance.createdBy !== undefined)
|
|
28
|
+
entry.createdBy.add(instance.createdBy);
|
|
29
|
+
for (const child of instance.renders)
|
|
30
|
+
entry.renders.add(child);
|
|
31
|
+
for (const token of instance.tokens)
|
|
32
|
+
entry.tokens.add(token);
|
|
33
|
+
const key = instance.props ?? UNKNOWN_PROPS;
|
|
34
|
+
const byProps = entry.classes.get(key) ?? new Map();
|
|
35
|
+
const rendering = byProps.get(instance.rendering) ?? {
|
|
36
|
+
rendering: instance.rendering,
|
|
37
|
+
structure: instance.structure,
|
|
38
|
+
semantics: instance.semantics,
|
|
39
|
+
text: instance.text,
|
|
40
|
+
style: instance.style,
|
|
41
|
+
renders: instance.renders,
|
|
42
|
+
sites: [],
|
|
43
|
+
};
|
|
44
|
+
rendering.sites.push({
|
|
45
|
+
subject: subject.subject,
|
|
46
|
+
path: instance.path,
|
|
47
|
+
depth: instance.depth,
|
|
48
|
+
...(instance.within === undefined ? {} : { within: instance.within }),
|
|
49
|
+
...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),
|
|
50
|
+
});
|
|
51
|
+
byProps.set(instance.rendering, rendering);
|
|
52
|
+
entry.classes.set(key, byProps);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const components = [...entries.entries()]
|
|
56
|
+
.map(([component, entry]) => ({
|
|
57
|
+
component,
|
|
58
|
+
subjects: entry.subjects,
|
|
59
|
+
instances: entry.instances,
|
|
60
|
+
examples: entry.examples,
|
|
61
|
+
within: [...entry.within].sort(byCodeUnit),
|
|
62
|
+
createdBy: [...entry.createdBy].sort(byCodeUnit),
|
|
63
|
+
renders: [...entry.renders].sort(byCodeUnit),
|
|
64
|
+
tokens: [...entry.tokens].sort(byCodeUnit),
|
|
65
|
+
classes: [...entry.classes.entries()]
|
|
66
|
+
.sort(([a], [b]) => byCodeUnit(a, b))
|
|
67
|
+
.map(([props, renderings]) => ({
|
|
68
|
+
...(props === UNKNOWN_PROPS ? {} : { props }),
|
|
69
|
+
renderings: [...renderings.values()].sort(widestFirst),
|
|
70
|
+
})),
|
|
71
|
+
}))
|
|
72
|
+
.sort((a, b) => byCodeUnit(a.component, b.component));
|
|
73
|
+
const snapshots = new Map();
|
|
74
|
+
for (const subject of subjects) {
|
|
75
|
+
if (subject.snapshot !== undefined)
|
|
76
|
+
snapshots.set(subject.subject, subject.snapshot);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
subjects: order,
|
|
80
|
+
components,
|
|
81
|
+
echoes: echoesOf(components),
|
|
82
|
+
divergences: divergencesOf(components, snapshots),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Every rendering shared by two or more subjects.
|
|
87
|
+
*
|
|
88
|
+
* Two instances of one component inside a *single* subject sharing a rendering
|
|
89
|
+
* is ordinary and says nothing — a list of three identical chips is three
|
|
90
|
+
* identical chips. The finding is a rendering that survives being mounted
|
|
91
|
+
* somewhere else, because that is the one a second subject is watching.
|
|
92
|
+
*/
|
|
93
|
+
function echoesOf(components) {
|
|
94
|
+
const echoes = [];
|
|
95
|
+
for (const entry of components) {
|
|
96
|
+
for (const group of entry.classes) {
|
|
97
|
+
for (const rendering of group.renderings) {
|
|
98
|
+
const spread = new Set(rendering.sites.map((site) => site.subject));
|
|
99
|
+
if (spread.size < 2)
|
|
100
|
+
continue;
|
|
101
|
+
const example = entry.examples.find((subject) => spread.has(subject));
|
|
102
|
+
echoes.push({
|
|
103
|
+
component: entry.component,
|
|
104
|
+
...(group.props === undefined ? {} : { props: group.props }),
|
|
105
|
+
rendering: rendering.rendering,
|
|
106
|
+
sites: rendering.sites,
|
|
107
|
+
...(example === undefined ? {} : { example }),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return echoes.sort((a, b) => b.sites.length - a.sites.length || byCodeUnit(a.component, b.component));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The component a subject exists to show, when one component does.
|
|
116
|
+
*
|
|
117
|
+
* The shallowest attributed boundary, and only when it is alone at that depth.
|
|
118
|
+
* A story usually mounts one thing; a page mounts a layout that mounts several,
|
|
119
|
+
* and calling the first of them the subject's component would be picking a
|
|
120
|
+
* winner out of document order.
|
|
121
|
+
*/
|
|
122
|
+
function exampleOf(instances) {
|
|
123
|
+
const shallowest = instances.filter(attributed).reduce((depth, instance) => {
|
|
124
|
+
return Math.min(depth, instance.depth);
|
|
125
|
+
}, Number.POSITIVE_INFINITY);
|
|
126
|
+
const leads = instances.filter((instance) => attributed(instance) && instance.depth === shallowest);
|
|
127
|
+
const names = new Set(leads.map((instance) => instance.component));
|
|
128
|
+
return leads.length === 1 || names.size === 1 ? leads[0]?.component : undefined;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The key an instance with no props digest is filed under.
|
|
132
|
+
*
|
|
133
|
+
* A sentinel rather than `undefined`, because a `Map` keyed on `Digest |
|
|
134
|
+
* undefined` reads as though unknown props were a props class like any other,
|
|
135
|
+
* and every consumer would then have to remember that it is not. It sorts last
|
|
136
|
+
* by construction — no digest starts with a space.
|
|
137
|
+
*/
|
|
138
|
+
const UNKNOWN_PROPS = ' unknown';
|
|
139
|
+
function accumulate(entries, component) {
|
|
140
|
+
const existing = entries.get(component);
|
|
141
|
+
if (existing !== undefined)
|
|
142
|
+
return existing;
|
|
143
|
+
const created = {
|
|
144
|
+
subjects: [],
|
|
145
|
+
instances: 0,
|
|
146
|
+
examples: [],
|
|
147
|
+
within: new Set(),
|
|
148
|
+
createdBy: new Set(),
|
|
149
|
+
renders: new Set(),
|
|
150
|
+
tokens: new Set(),
|
|
151
|
+
classes: new Map(),
|
|
152
|
+
};
|
|
153
|
+
entries.set(component, created);
|
|
154
|
+
return created;
|
|
155
|
+
}
|
|
156
|
+
function widestFirst(a, b) {
|
|
157
|
+
return b.sites.length - a.sites.length || byCodeUnit(a.rendering, b.rendering);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Code-unit order, never `localeCompare`.
|
|
161
|
+
*
|
|
162
|
+
* The same rule `hashComponents` sorts by, for the same reason: this output
|
|
163
|
+
* reaches a report that is committed, diffed and read back on another runner,
|
|
164
|
+
* and a locale-aware comparison makes the byte order a promise about `LANG`.
|
|
165
|
+
*/
|
|
166
|
+
function byCodeUnit(a, b) {
|
|
167
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=composition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition.js","sourceRoot":"","sources":["../../src/attribute/composition.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAmB,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,UAAU,EAA0B,MAAM,gBAAgB,CAAC;AA8LpE;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,QAAuC;IACrE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAEzD,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAE7C,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;YACzC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAEpC,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;YACtD,IAAI,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,OAAO;gBAAE,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACpF,KAAK,CAAC,SAAS,IAAI,CAAC,CAAC;YACrB,IAAI,OAAO,KAAK,QAAQ,CAAC,SAAS,IAAI,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC;gBAChF,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACvC,CAAC;YACD,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS;gBAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACrE,IAAI,QAAQ,CAAC,SAAS,KAAK,SAAS;gBAAE,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YAC9E,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,OAAO;gBAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC/D,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,MAAM;gBAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAE7D,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,IAAI,aAAa,CAAC;YAC5C,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,EAA4B,CAAC;YAC9E,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI;gBACnD,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,OAAO,EAAE,QAAQ,CAAC,OAAO;gBACzB,KAAK,EAAE,EAAE;aACV,CAAC;YACF,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,GAAG,CAAC,QAAQ,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACrE,GAAG,CAAC,QAAQ,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC;aAC/E,CAAC,CAAC;YACH,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YAC3C,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,CAAC,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5B,SAAS;QACT,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,MAAM,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QAC1C,SAAS,EAAE,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QAChD,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QAC5C,MAAM,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QAC1C,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;aAClC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;aACpC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC;YAC7B,GAAG,CAAC,KAAK,KAAK,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;YAC7C,UAAU,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC;SACvD,CAAC,CAAC;KACN,CAAC,CAAC;SACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAExD,MAAM,SAAS,GAAG,IAAI,GAAG,EAA4B,CAAC;IACtD,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS;YAAE,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvF,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,KAAK;QACf,UAAU;QACV,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC;QAC5B,WAAW,EAAE,aAAa,CAAC,UAAU,EAAE,SAAS,CAAC;KAClD,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,QAAQ,CAAC,UAAqC;IACrD,MAAM,MAAM,GAAW,EAAE,CAAC;IAE1B,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;QAC/B,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YAClC,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;gBACzC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;gBACpE,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC;oBAAE,SAAS;gBAE9B,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;gBACtE,MAAM,CAAC,IAAI,CAAC;oBACV,SAAS,EAAE,KAAK,CAAC,SAAS;oBAC1B,GAAG,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;oBAC5D,SAAS,EAAE,SAAS,CAAC,SAAS;oBAC9B,KAAK,EAAE,SAAS,CAAC,KAAK;oBACtB,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;iBAC9C,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAChB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,UAAU,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAClF,CAAC;AACJ,CAAC;AAGD;;;;;;;GAOG;AACH,SAAS,SAAS,CAAC,SAAuC;IACxD,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;QACjF,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACzC,CAAC,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC;IAE7B,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC;IACpG,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IACnE,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;AAClF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,aAAa,GAAG,UAAU,CAAC;AAkBjC,SAAS,UAAU,CAAC,OAAiC,EAAE,SAAiB;IACtE,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACxC,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IAE5C,MAAM,OAAO,GAAgB;QAC3B,QAAQ,EAAE,EAAE;QACZ,SAAS,EAAE,CAAC;QACZ,QAAQ,EAAE,EAAE;QACZ,MAAM,EAAE,IAAI,GAAG,EAAE;QACjB,SAAS,EAAE,IAAI,GAAG,EAAE;QACpB,OAAO,EAAE,IAAI,GAAG,EAAE;QAClB,MAAM,EAAE,IAAI,GAAG,EAAE;QACjB,OAAO,EAAE,IAAI,GAAG,EAAE;KACnB,CAAC;IACF,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAChC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,WAAW,CAAC,CAAY,EAAE,CAAY;IAC7C,OAAO,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,UAAU,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;AACjF,CAAC;AAED;;;;;;GAMG;AACH,SAAS,UAAU,CAAC,CAAS,EAAE,CAAS;IACtC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC","sourcesContent":["import type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticSnapshot } from '../format/snapshot.js';\nimport { divergencesOf, type Divergence } from './divergence.js';\nimport { attributed, type ComponentInstance } from './instances.js';\n\n/**\n * The suite read as one graph, at one commit.\n *\n * Every other comparison in this system is between two revisions of one subject.\n * This is the other axis: **many subjects, one revision**, joined on the\n * components they share. It answers the question a per-subject tool cannot even\n * phrase — *this component, here, is the same component you already have an\n * example of over there* — and it answers it from digests a run already\n * computed, with no second render, no image and no store.\n *\n * ## What the join is worth\n *\n * A suite of examples is a suite of *compositions*. The narrow example is a\n * component at a boundary; the page examples contain that same boundary again,\n * sometimes with the same props and sometimes not. Once the instances are\n * addressable, three facts fall out that nobody had:\n *\n * - **An echo.** One rendering, several subjects. The narrow example and the\n * page are watching literally the same bytes, so a change in one is a change\n * in all of them, and a reviewer looking at eleven diffs is looking at one.\n * - **A divergence.** Same component, same props, *different* rendering, at one\n * commit. Nothing about the component's own inputs can explain that. Either\n * something outside it decides its output — an ancestor's cascade, a token, its\n * own state — or the reading is not repeatable. Both are findings and neither\n * is a regression.\n * - **A control group.** For any instance that moved, the same rendering\n * somewhere else that did *not* is a stable state to refer to. That is the\n * referent flake attribution has always needed and never had, and the suite\n * supplies it for free: one commit already holds N renderings of every shared\n * component.\n *\n * ## What it deliberately does not do\n *\n * It never decides anything. Two subjects sharing a rendering is not a reason to\n * delete either — that is the argument `coverage.ts` makes at the level of names\n * and it holds harder here, because a component can be correct in one context\n * and broken in the next, which is why the contexts are separate subjects.\n */\n\n/** One instance, located. */\nexport interface Site {\n readonly subject: string;\n readonly path: NodePath;\n readonly depth: number;\n readonly within?: string;\n\n /**\n * The component whose JSX wrote this element, here.\n *\n * On the entry beside it, `createdBy` is a set folded over the whole suite —\n * every component that ever wrote one of these, anywhere. That is the right\n * shape for *who mounts this* and the wrong one for *who mounted this here*,\n * and the ladder asks the second: an edit to a component that writes a `Chip`\n * on the footer explains nothing about a `Chip` the sidebar wrote. Kept per\n * site so the question can be asked where it was answered.\n */\n readonly createdBy?: string;\n}\n\n/** One rendering of one component, and everywhere it occurred. */\nexport interface Rendering {\n readonly rendering: Digest;\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n\n /**\n * Child components this rendering mounted, in document order.\n *\n * Carried on the rendering rather than only on the entry because it is the\n * only evidence available about what was passed *in*: `propsDigest` excludes\n * `children` by design, so two renderings that mount different children are\n * two different inputs wearing one props digest. `divergencesOf` refuses to\n * call that a contradiction, and this is the field it reads.\n */\n readonly renders: readonly string[];\n\n /** In subject order, then document order. */\n readonly sites: readonly Site[];\n}\n\n/** One props digest of one component, and every way it rendered under it. */\nexport interface PropsClass {\n /** Absent when the collector supplied no provenance — unknown, not \"none\". */\n readonly props?: Digest;\n readonly renderings: readonly Rendering[];\n}\n\nexport interface ComponentEntry {\n readonly component: string;\n /** Subjects containing at least one boundary of it, in the order supplied. */\n readonly subjects: readonly string[];\n /** Boundaries summed across every subject. Distinct from `subjects.length`. */\n readonly instances: number;\n\n /**\n * Subjects whose shallowest attributed boundary is this component.\n *\n * The narrow example — the subject that exists to show this thing, rather than\n * a page that happens to contain it. Defined by depth rather than by a naming\n * convention because a naming convention is a different tool's `story:` prefix,\n * and this has to hold for a route suite too. Ties resolve to nothing: a\n * subject whose two shallowest boundaries are siblings has no single subject.\n */\n readonly examples: readonly string[];\n\n /** Components that enclose it somewhere, sorted. The graph, upwards. */\n readonly within: readonly string[];\n\n /**\n * Components that *mounted* it somewhere, sorted. The graph, upwards, again —\n * and usually the more useful of the two.\n *\n * `within` is where the boundary sits in the DOM and this is who wrote the\n * element. They are the same thing only for a component that authors a node of\n * its own, and a real application is full of components that do not: measured\n * on `examples/todomvc`, every `Chip` reports `within: ['Stack']` and\n * `createdBy: ['TodoFooter']`. `TodoFooter` renders nothing but other\n * components, so it owns no node, is a boundary nowhere, and appears in this\n * graph *only* here — while being the file a reviewer has to open.\n *\n * Empty on a production build, where `_debugOwner` is absent (ADR-0007). Empty\n * is *not* \"nothing mounted it\": a component with no caller is the subject root\n * and every other case is missing data, which is why the `upstream` rung in\n * `movement.ts` reads this and never concludes from its silence.\n */\n readonly createdBy: readonly string[];\n\n /** Components it encloses somewhere, sorted. The graph, downwards. */\n readonly renders: readonly string[];\n\n /**\n * Custom properties its own nodes resolve through, anywhere in the suite.\n *\n * The set a token movement is intersected against, which is how \"`--va-space-3`\n * moved\" becomes \"`--va-space-3` moved and these four components read it\".\n */\n readonly tokens: readonly string[];\n\n /** Sorted by props digest; the class with unknown props sorts last. */\n readonly classes: readonly PropsClass[];\n}\n\n/**\n * The same component, the same props, the same bytes, in more than one subject.\n *\n * The connected dots. `example` names the narrow subject among the sites when\n * there is one, because that is the artefact a reviewer already has an opinion\n * about — \"this is the Chip from the Chip story\" is a sentence, and \"these four\n * paths share a digest\" is not.\n */\nexport interface Echo {\n readonly component: string;\n readonly props?: Digest;\n readonly rendering: Digest;\n /** At least two, spanning at least two subjects. */\n readonly sites: readonly Site[];\n readonly example?: string;\n}\n\n\nexport interface SubjectComposition {\n readonly subject: string;\n readonly instances: readonly ComponentInstance[];\n\n /**\n * The subject as read, kept so a divergence can say *which input* moved.\n *\n * Optional because `composeSubjects` is a fold over instance lists and stays\n * one — a caller holding only a report's sidecars still gets the graph, the\n * echoes and the divergences, and gets them without a `partings` field it\n * would have to explain away as empty.\n */\n readonly snapshot?: SemanticSnapshot;\n}\n\nexport interface Composition {\n /** In the order supplied, which the run guarantees is plan order. */\n readonly subjects: readonly string[];\n /** Sorted by name, code-unit order. */\n readonly components: readonly ComponentEntry[];\n /** Sorted by how many sites each has, widest first, then by component. */\n readonly echoes: readonly Echo[];\n /** Sorted by component name. */\n readonly divergences: readonly Divergence[];\n}\n\n/**\n * Fold a run's per-subject instance lists into the graph.\n *\n * Pure, ordered, and a function of its input alone — the run's report has to be\n * a function of the plan, and a phase that accumulated as a worker pool finished\n * would produce a different artefact from the same suite on a slower machine.\n */\nexport function composeSubjects(subjects: readonly SubjectComposition[]): Composition {\n const entries = new Map<string, Accumulator>();\n const order = subjects.map((subject) => subject.subject);\n\n for (const subject of subjects) {\n const example = exampleOf(subject.instances);\n\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n\n const entry = accumulate(entries, instance.component);\n if (entry.subjects.at(-1) !== subject.subject) entry.subjects.push(subject.subject);\n entry.instances += 1;\n if (example === instance.component && entry.examples.at(-1) !== subject.subject) {\n entry.examples.push(subject.subject);\n }\n if (instance.within !== undefined) entry.within.add(instance.within);\n if (instance.createdBy !== undefined) entry.createdBy.add(instance.createdBy);\n for (const child of instance.renders) entry.renders.add(child);\n for (const token of instance.tokens) entry.tokens.add(token);\n\n const key = instance.props ?? UNKNOWN_PROPS;\n const byProps = entry.classes.get(key) ?? new Map<Digest, MutableRendering>();\n const rendering = byProps.get(instance.rendering) ?? {\n rendering: instance.rendering,\n structure: instance.structure,\n semantics: instance.semantics,\n text: instance.text,\n style: instance.style,\n renders: instance.renders,\n sites: [],\n };\n rendering.sites.push({\n subject: subject.subject,\n path: instance.path,\n depth: instance.depth,\n ...(instance.within === undefined ? {} : { within: instance.within }),\n ...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),\n });\n byProps.set(instance.rendering, rendering);\n entry.classes.set(key, byProps);\n }\n }\n\n const components = [...entries.entries()]\n .map(([component, entry]) => ({\n component,\n subjects: entry.subjects,\n instances: entry.instances,\n examples: entry.examples,\n within: [...entry.within].sort(byCodeUnit),\n createdBy: [...entry.createdBy].sort(byCodeUnit),\n renders: [...entry.renders].sort(byCodeUnit),\n tokens: [...entry.tokens].sort(byCodeUnit),\n classes: [...entry.classes.entries()]\n .sort(([a], [b]) => byCodeUnit(a, b))\n .map(([props, renderings]) => ({\n ...(props === UNKNOWN_PROPS ? {} : { props }),\n renderings: [...renderings.values()].sort(widestFirst),\n })),\n }))\n .sort((a, b) => byCodeUnit(a.component, b.component));\n\n const snapshots = new Map<string, SemanticSnapshot>();\n for (const subject of subjects) {\n if (subject.snapshot !== undefined) snapshots.set(subject.subject, subject.snapshot);\n }\n\n return {\n subjects: order,\n components,\n echoes: echoesOf(components),\n divergences: divergencesOf(components, snapshots),\n };\n}\n\n/**\n * Every rendering shared by two or more subjects.\n *\n * Two instances of one component inside a *single* subject sharing a rendering\n * is ordinary and says nothing — a list of three identical chips is three\n * identical chips. The finding is a rendering that survives being mounted\n * somewhere else, because that is the one a second subject is watching.\n */\nfunction echoesOf(components: readonly ComponentEntry[]): readonly Echo[] {\n const echoes: Echo[] = [];\n\n for (const entry of components) {\n for (const group of entry.classes) {\n for (const rendering of group.renderings) {\n const spread = new Set(rendering.sites.map((site) => site.subject));\n if (spread.size < 2) continue;\n\n const example = entry.examples.find((subject) => spread.has(subject));\n echoes.push({\n component: entry.component,\n ...(group.props === undefined ? {} : { props: group.props }),\n rendering: rendering.rendering,\n sites: rendering.sites,\n ...(example === undefined ? {} : { example }),\n });\n }\n }\n }\n\n return echoes.sort(\n (a, b) => b.sites.length - a.sites.length || byCodeUnit(a.component, b.component),\n );\n}\n\n\n/**\n * The component a subject exists to show, when one component does.\n *\n * The shallowest attributed boundary, and only when it is alone at that depth.\n * A story usually mounts one thing; a page mounts a layout that mounts several,\n * and calling the first of them the subject's component would be picking a\n * winner out of document order.\n */\nfunction exampleOf(instances: readonly ComponentInstance[]): string | undefined {\n const shallowest = instances.filter(attributed).reduce<number>((depth, instance) => {\n return Math.min(depth, instance.depth);\n }, Number.POSITIVE_INFINITY);\n\n const leads = instances.filter((instance) => attributed(instance) && instance.depth === shallowest);\n const names = new Set(leads.map((instance) => instance.component));\n return leads.length === 1 || names.size === 1 ? leads[0]?.component : undefined;\n}\n\n/**\n * The key an instance with no props digest is filed under.\n *\n * A sentinel rather than `undefined`, because a `Map` keyed on `Digest |\n * undefined` reads as though unknown props were a props class like any other,\n * and every consumer would then have to remember that it is not. It sorts last\n * by construction — no digest starts with a space.\n */\nconst UNKNOWN_PROPS = ' unknown';\n\ninterface Accumulator {\n readonly subjects: string[];\n instances: number;\n readonly examples: string[];\n readonly within: Set<string>;\n readonly createdBy: Set<string>;\n readonly renders: Set<string>;\n readonly tokens: Set<string>;\n readonly classes: Map<string, Map<Digest, MutableRendering>>;\n}\n\n/** A `Rendering` while its sites are still being collected. */\ninterface MutableRendering extends Omit<Rendering, 'sites'> {\n readonly sites: Site[];\n}\n\nfunction accumulate(entries: Map<string, Accumulator>, component: string): Accumulator {\n const existing = entries.get(component);\n if (existing !== undefined) return existing;\n\n const created: Accumulator = {\n subjects: [],\n instances: 0,\n examples: [],\n within: new Set(),\n createdBy: new Set(),\n renders: new Set(),\n tokens: new Set(),\n classes: new Map(),\n };\n entries.set(component, created);\n return created;\n}\n\nfunction widestFirst(a: Rendering, b: Rendering): number {\n return b.sites.length - a.sites.length || byCodeUnit(a.rendering, b.rendering);\n}\n\n/**\n * Code-unit order, never `localeCompare`.\n *\n * The same rule `hashComponents` sorts by, for the same reason: this output\n * reaches a report that is committed, diffed and read back on another runner,\n * and a locale-aware comparison makes the byte order a promise about `LANG`.\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
|
+
* The renders a change is measured against — the arm a diff cannot see.
|
|
3
|
+
*
|
|
4
|
+
* A comparison sees the population that changed. This is the other one: sites of
|
|
5
|
+
* the same component, under the *same props*, that this run read and found
|
|
6
|
+
* unmoved. It is what makes an unexplained movement a finding rather than a
|
|
7
|
+
* shrug, and what bounds an explained one — `Button` was edited and held in four
|
|
8
|
+
* renders says the edit was to a variant those four do not use, with evidence
|
|
9
|
+
* rather than with a count of what happened to be collected.
|
|
10
|
+
*
|
|
11
|
+
* ## The props class is the load-bearing half
|
|
12
|
+
*
|
|
13
|
+
* Two renders of a component given different inputs are not controls for each
|
|
14
|
+
* other. They are two questions that share a name, and answering one with the
|
|
15
|
+
* other is how a control group becomes a source of confident wrong answers.
|
|
16
|
+
*
|
|
17
|
+
* ## What disqualifies a site
|
|
18
|
+
*
|
|
19
|
+
* Two things, and only the first is obvious. It moved — either the run reported
|
|
20
|
+
* it, or its hashes say so in a render where no region named it, which is the
|
|
21
|
+
* majority case and the one that made every entry in this list wrong before
|
|
22
|
+
* `hashesMoved` was carried. Or nobody looked: the subject was collected with no
|
|
23
|
+
* baseline digests to compare, and *unmeasured* is not *unchanged*.
|
|
24
|
+
*/
|
|
25
|
+
import type { ComponentEntry, Site } from './composition.js';
|
|
26
|
+
import type { Bench, Moved } from './movement.js';
|
|
27
|
+
/**
|
|
28
|
+
* Sites of the same component this run read and found unmoved.
|
|
29
|
+
*
|
|
30
|
+
* Restricted to props classes this subject actually participates in, because a
|
|
31
|
+
* component rendered with different inputs elsewhere is not a control for this
|
|
32
|
+
* one — it is a different question that happens to share a name.
|
|
33
|
+
*
|
|
34
|
+
* Two ways a site fails to be a control, and only the second is obvious. It
|
|
35
|
+
* moved: the run reported it, or its hashes say so even where no region named
|
|
36
|
+
* it. Or nobody looked: the subject was collected with no baseline hashes to
|
|
37
|
+
* compare, and *unmeasured* is not *unchanged*.
|
|
38
|
+
*/
|
|
39
|
+
export declare function heldSites(moved: Moved, entry: ComponentEntry | undefined, bench: Bench): {
|
|
40
|
+
readonly held: readonly Site[];
|
|
41
|
+
readonly considered: number;
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=control.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renders a change is measured against — the arm a diff cannot see.
|
|
3
|
+
*
|
|
4
|
+
* A comparison sees the population that changed. This is the other one: sites of
|
|
5
|
+
* the same component, under the *same props*, that this run read and found
|
|
6
|
+
* unmoved. It is what makes an unexplained movement a finding rather than a
|
|
7
|
+
* shrug, and what bounds an explained one — `Button` was edited and held in four
|
|
8
|
+
* renders says the edit was to a variant those four do not use, with evidence
|
|
9
|
+
* rather than with a count of what happened to be collected.
|
|
10
|
+
*
|
|
11
|
+
* ## The props class is the load-bearing half
|
|
12
|
+
*
|
|
13
|
+
* Two renders of a component given different inputs are not controls for each
|
|
14
|
+
* other. They are two questions that share a name, and answering one with the
|
|
15
|
+
* other is how a control group becomes a source of confident wrong answers.
|
|
16
|
+
*
|
|
17
|
+
* ## What disqualifies a site
|
|
18
|
+
*
|
|
19
|
+
* Two things, and only the first is obvious. It moved — either the run reported
|
|
20
|
+
* it, or its hashes say so in a render where no region named it, which is the
|
|
21
|
+
* majority case and the one that made every entry in this list wrong before
|
|
22
|
+
* `hashesMoved` was carried. Or nobody looked: the subject was collected with no
|
|
23
|
+
* baseline digests to compare, and *unmeasured* is not *unchanged*.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Sites of the same component this run read and found unmoved.
|
|
27
|
+
*
|
|
28
|
+
* Restricted to props classes this subject actually participates in, because a
|
|
29
|
+
* component rendered with different inputs elsewhere is not a control for this
|
|
30
|
+
* one — it is a different question that happens to share a name.
|
|
31
|
+
*
|
|
32
|
+
* Two ways a site fails to be a control, and only the second is obvious. It
|
|
33
|
+
* moved: the run reported it, or its hashes say so even where no region named
|
|
34
|
+
* it. Or nobody looked: the subject was collected with no baseline hashes to
|
|
35
|
+
* compare, and *unmeasured* is not *unchanged*.
|
|
36
|
+
*/
|
|
37
|
+
export function heldSites(moved, entry, bench) {
|
|
38
|
+
if (entry === undefined)
|
|
39
|
+
return { held: [], considered: 0 };
|
|
40
|
+
const alsoMoved = new Set(bench.movedIn.get(moved.component) ?? []);
|
|
41
|
+
const hashes = bench.evidence.hashesMoved;
|
|
42
|
+
const elsewhere = entry.classes
|
|
43
|
+
.filter((group) => group.renderings.some((rendering) => rendering.sites.some((site) => site.subject === moved.subject)))
|
|
44
|
+
.flatMap((group) => group.renderings)
|
|
45
|
+
.flatMap((rendering) => rendering.sites)
|
|
46
|
+
.filter((site) => site.subject !== moved.subject);
|
|
47
|
+
return {
|
|
48
|
+
held: elsewhere
|
|
49
|
+
.filter((site) => !alsoMoved.has(site.subject))
|
|
50
|
+
.filter((site) => hashes === undefined || hashes.get(site.subject)?.has(moved.component) === false),
|
|
51
|
+
considered: new Set(elsewhere.map((site) => site.subject)).size,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control.js","sourceRoot":"","sources":["../../src/attribute/control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,SAAS,CACvB,KAAY,EACZ,KAAiC,EACjC,KAAY;IAEZ,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IAE5D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;IACpE,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC;IAE1C,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO;SAC5B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAChB,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAClC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO,CAAC,CAC/D,CACF;SACA,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC;SACpC,OAAO,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC;SACvC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC;IAEpD,OAAO;QACL,IAAI,EAAE,SAAS;aACZ,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;aAC9C,MAAM,CACL,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,KAAK,CAC3F;QACH,UAAU,EAAE,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;KAChE,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The renders a change is measured against — the arm a diff cannot see.\n *\n * A comparison sees the population that changed. This is the other one: sites of\n * the same component, under the *same props*, that this run read and found\n * unmoved. It is what makes an unexplained movement a finding rather than a\n * shrug, and what bounds an explained one — `Button` was edited and held in four\n * renders says the edit was to a variant those four do not use, with evidence\n * rather than with a count of what happened to be collected.\n *\n * ## The props class is the load-bearing half\n *\n * Two renders of a component given different inputs are not controls for each\n * other. They are two questions that share a name, and answering one with the\n * other is how a control group becomes a source of confident wrong answers.\n *\n * ## What disqualifies a site\n *\n * Two things, and only the first is obvious. It moved — either the run reported\n * it, or its hashes say so in a render where no region named it, which is the\n * majority case and the one that made every entry in this list wrong before\n * `hashesMoved` was carried. Or nobody looked: the subject was collected with no\n * baseline digests to compare, and *unmeasured* is not *unchanged*.\n */\n\nimport type { ComponentEntry, Site } from './composition.js';\nimport type { Bench, Moved } from './movement.js';\n\n/**\n * Sites of the same component this run read and found unmoved.\n *\n * Restricted to props classes this subject actually participates in, because a\n * component rendered with different inputs elsewhere is not a control for this\n * one — it is a different question that happens to share a name.\n *\n * Two ways a site fails to be a control, and only the second is obvious. It\n * moved: the run reported it, or its hashes say so even where no region named\n * it. Or nobody looked: the subject was collected with no baseline hashes to\n * compare, and *unmeasured* is not *unchanged*.\n */\nexport function heldSites(\n moved: Moved,\n entry: ComponentEntry | undefined,\n bench: Bench,\n): { readonly held: readonly Site[]; readonly considered: number } {\n if (entry === undefined) return { held: [], considered: 0 };\n\n const alsoMoved = new Set(bench.movedIn.get(moved.component) ?? []);\n const hashes = bench.evidence.hashesMoved;\n\n const elsewhere = entry.classes\n .filter((group) =>\n group.renderings.some((rendering) =>\n rendering.sites.some((site) => site.subject === moved.subject),\n ),\n )\n .flatMap((group) => group.renderings)\n .flatMap((rendering) => rendering.sites)\n .filter((site) => site.subject !== moved.subject);\n\n return {\n held: elsewhere\n .filter((site) => !alsoMoved.has(site.subject))\n .filter(\n (site) => hashes === undefined || hashes.get(site.subject)?.has(moved.component) === false,\n ),\n considered: new Set(elsewhere.map((site) => site.subject)).size,\n };\n}\n"]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ComponentHash } from './component-hash.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which subjects cover which components, and what each subject covers alone.
|
|
4
|
+
*
|
|
5
|
+
* A visual-regression suite accretes. A component gets a narrow subject of its
|
|
6
|
+
* own, then appears inside four page subjects, then inside a dozen. Every one of
|
|
7
|
+
* those subjects costs a capture on every run, and nothing in a per-subject tool
|
|
8
|
+
* can say whether the narrow one is still earning its place — that question is
|
|
9
|
+
* about the *set*, and no single subject knows it is a member of one.
|
|
10
|
+
*
|
|
11
|
+
* The two decisions this enables are opposites, and which one applies is a
|
|
12
|
+
* judgement the data cannot make:
|
|
13
|
+
*
|
|
14
|
+
* - A component covered by many subjects may not need its own. Or the broad
|
|
15
|
+
* subjects may be the ones that should stop looking at it, so that a change
|
|
16
|
+
* inside it is reported once rather than a dozen times.
|
|
17
|
+
* - A component covered by exactly one subject is the opposite risk. Deleting
|
|
18
|
+
* that subject removes the only thing watching it, and nothing else in the
|
|
19
|
+
* suite will notice when it breaks.
|
|
20
|
+
*
|
|
21
|
+
* **This never decides anything.** A component rendered in two contexts can be
|
|
22
|
+
* correct in one and broken in the other, which is the entire reason contexts are
|
|
23
|
+
* captured separately — so overlap is not redundancy, and a tool that deleted
|
|
24
|
+
* subjects on this signal would remove real coverage on the strength of a name
|
|
25
|
+
* matching. What is produced here is the list a person needs in order to make
|
|
26
|
+
* that call, and the honest form of it is a count next to a name.
|
|
27
|
+
*
|
|
28
|
+
* Cheap by construction: it reads the component hashes a run already produced
|
|
29
|
+
* (see {@link ./component-hash.js}) and touches no DOM, no browser and no image.
|
|
30
|
+
*
|
|
31
|
+
* ## What `composeSubjects` took from this, and what it left
|
|
32
|
+
*
|
|
33
|
+
* {@link ./composition.js} answers the same census question from richer input —
|
|
34
|
+
* instances with provenance rather than hashes — and it is the one a run report
|
|
35
|
+
* carries, so a component's subjects and instance count are now computed twice
|
|
36
|
+
* from two inputs. If they ever disagree, `composeSubjects` is the one that is
|
|
37
|
+
* right: it is what `variance run` writes and what every reader downstream of
|
|
38
|
+
* the artifact is reading. `sole` there is `subjects.length === 1`.
|
|
39
|
+
*
|
|
40
|
+
* What survives here and nowhere else is {@link SubjectValue.unique} — the
|
|
41
|
+
* components *no other subject in the set covers*. That is a question about a
|
|
42
|
+
* subject rather than about a component, and composing answers the other one:
|
|
43
|
+
* `examples` names the narrowest subject watching a component, which is not the
|
|
44
|
+
* same as naming the only one. A suite pruning its own captures needs this; a
|
|
45
|
+
* run explaining a diff needs that.
|
|
46
|
+
*/
|
|
47
|
+
/** What one subject was found to contain. */
|
|
48
|
+
export interface SubjectCoverage {
|
|
49
|
+
readonly subject: string;
|
|
50
|
+
readonly components: readonly ComponentHash[];
|
|
51
|
+
}
|
|
52
|
+
export interface ComponentCoverage {
|
|
53
|
+
readonly component: string;
|
|
54
|
+
/** Subjects containing at least one boundary of it, in the order supplied. */
|
|
55
|
+
readonly subjects: readonly string[];
|
|
56
|
+
/** Boundaries summed across every subject. Distinct from `subjects.length`. */
|
|
57
|
+
readonly instances: number;
|
|
58
|
+
/**
|
|
59
|
+
* `true` when exactly one subject covers this component.
|
|
60
|
+
*
|
|
61
|
+
* The signal that a subject must not be deleted casually, rather than a
|
|
62
|
+
* complaint. Sole coverage is normal and often correct.
|
|
63
|
+
*/
|
|
64
|
+
readonly sole: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface SubjectValue {
|
|
67
|
+
readonly subject: string;
|
|
68
|
+
readonly components: number;
|
|
69
|
+
/**
|
|
70
|
+
* Components no other subject in the set covers.
|
|
71
|
+
*
|
|
72
|
+
* Empty does **not** mean the subject is redundant — it means every component
|
|
73
|
+
* it contains is watched somewhere else *as a component*, which says nothing
|
|
74
|
+
* about whether this arrangement of them renders correctly. It is the
|
|
75
|
+
* beginning of the question, not the answer.
|
|
76
|
+
*/
|
|
77
|
+
readonly unique: readonly string[];
|
|
78
|
+
}
|
|
79
|
+
export interface Coverage {
|
|
80
|
+
/** Sorted by how many subjects cover each, widest first, then by name. */
|
|
81
|
+
readonly components: readonly ComponentCoverage[];
|
|
82
|
+
/** In the order the subjects were supplied. */
|
|
83
|
+
readonly subjects: readonly SubjectValue[];
|
|
84
|
+
}
|
|
85
|
+
export declare function coverageOf(subjects: readonly SubjectCoverage[]): Coverage;
|
|
86
|
+
/** Subjects that would still watch this component if one of them were removed. */
|
|
87
|
+
export declare function alsoCovering(coverage: Coverage, component: string, excluding: string): readonly string[];
|
|
88
|
+
/**
|
|
89
|
+
* The overlap as something a person reads before deciding.
|
|
90
|
+
*
|
|
91
|
+
* Leads with sole coverage rather than with the widest components, because the
|
|
92
|
+
* widest components are the ones a reader already knows about — they are in
|
|
93
|
+
* every screenshot — while a component watched by exactly one subject is the fact
|
|
94
|
+
* nobody has.
|
|
95
|
+
*/
|
|
96
|
+
export declare function summarizeCoverage(coverage: Coverage, options?: {
|
|
97
|
+
readonly limit?: number;
|
|
98
|
+
}): string;
|
|
99
|
+
//# sourceMappingURL=coverage.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export function coverageOf(subjects) {
|
|
2
|
+
const byComponent = new Map();
|
|
3
|
+
for (const subject of subjects) {
|
|
4
|
+
// A subject that names the same component twice would otherwise inflate its
|
|
5
|
+
// own coverage count. Boundaries are already folded per component upstream,
|
|
6
|
+
// but this function is public and takes whatever it is handed.
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
for (const hash of subject.components) {
|
|
9
|
+
const entry = byComponent.get(hash.component) ?? { subjects: [], instances: 0 };
|
|
10
|
+
if (!seen.has(hash.component)) {
|
|
11
|
+
entry.subjects.push(subject.subject);
|
|
12
|
+
seen.add(hash.component);
|
|
13
|
+
}
|
|
14
|
+
entry.instances += hash.instances;
|
|
15
|
+
byComponent.set(hash.component, entry);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const components = [...byComponent.entries()]
|
|
19
|
+
.map(([component, entry]) => ({
|
|
20
|
+
component,
|
|
21
|
+
subjects: entry.subjects,
|
|
22
|
+
instances: entry.instances,
|
|
23
|
+
sole: entry.subjects.length === 1,
|
|
24
|
+
}))
|
|
25
|
+
.sort((a, b) => b.subjects.length - a.subjects.length ||
|
|
26
|
+
(a.component < b.component ? -1 : a.component > b.component ? 1 : 0));
|
|
27
|
+
const soleOwner = new Map();
|
|
28
|
+
for (const component of components) {
|
|
29
|
+
if (component.sole)
|
|
30
|
+
soleOwner.set(component.component, component.subjects[0]);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
components,
|
|
34
|
+
subjects: subjects.map((subject) => {
|
|
35
|
+
const names = [...new Set(subject.components.map((hash) => hash.component))];
|
|
36
|
+
return {
|
|
37
|
+
subject: subject.subject,
|
|
38
|
+
components: names.length,
|
|
39
|
+
unique: names.filter((name) => soleOwner.get(name) === subject.subject),
|
|
40
|
+
};
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Subjects that would still watch this component if one of them were removed. */
|
|
45
|
+
export function alsoCovering(coverage, component, excluding) {
|
|
46
|
+
const entry = coverage.components.find((candidate) => candidate.component === component);
|
|
47
|
+
return entry === undefined ? [] : entry.subjects.filter((subject) => subject !== excluding);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The overlap as something a person reads before deciding.
|
|
51
|
+
*
|
|
52
|
+
* Leads with sole coverage rather than with the widest components, because the
|
|
53
|
+
* widest components are the ones a reader already knows about — they are in
|
|
54
|
+
* every screenshot — while a component watched by exactly one subject is the fact
|
|
55
|
+
* nobody has.
|
|
56
|
+
*/
|
|
57
|
+
export function summarizeCoverage(coverage, options = {}) {
|
|
58
|
+
const limit = options.limit ?? 10;
|
|
59
|
+
if (coverage.components.length === 0)
|
|
60
|
+
return 'no components covered';
|
|
61
|
+
const sole = coverage.components.filter((component) => component.sole);
|
|
62
|
+
const broad = coverage.components.filter((component) => component.subjects.length > 1);
|
|
63
|
+
const deletable = coverage.subjects.filter((subject) => subject.unique.length === 0);
|
|
64
|
+
const lines = [
|
|
65
|
+
`${coverage.components.length} component(s) across ${coverage.subjects.length} subject(s)`,
|
|
66
|
+
'',
|
|
67
|
+
`${sole.length} component(s) covered by exactly one subject:`,
|
|
68
|
+
...sole.slice(0, limit).map((c) => ` ${c.component} — only in ${c.subjects[0]}`),
|
|
69
|
+
...(sole.length > limit ? [` +${sole.length - limit} more not listed`] : []),
|
|
70
|
+
];
|
|
71
|
+
if (broad.length > 0) {
|
|
72
|
+
lines.push('', `${broad.length} component(s) covered by more than one subject, widest first:`, ...broad
|
|
73
|
+
.slice(0, limit)
|
|
74
|
+
.map((c) => ` ${c.component} — ${c.subjects.length} subject(s), ${c.instances} instance(s)`), ...(broad.length > limit ? [` +${broad.length - limit} more not listed`] : []));
|
|
75
|
+
}
|
|
76
|
+
if (deletable.length > 0) {
|
|
77
|
+
lines.push('', `${deletable.length} subject(s) cover no component alone: ` +
|
|
78
|
+
deletable
|
|
79
|
+
.slice(0, limit)
|
|
80
|
+
.map((s) => s.subject)
|
|
81
|
+
.join(', ') +
|
|
82
|
+
(deletable.length > limit ? `, +${deletable.length - limit} more` : ''), 'That is not a recommendation to delete them. A component can render correctly', 'in one context and wrongly in another, which is why the contexts are separate', 'subjects in the first place.');
|
|
83
|
+
}
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=coverage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../src/attribute/coverage.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentHash } from './component-hash.js';\n\n/**\n * Which subjects cover which components, and what each subject covers alone.\n *\n * A visual-regression suite accretes. A component gets a narrow subject of its\n * own, then appears inside four page subjects, then inside a dozen. Every one of\n * those subjects costs a capture on every run, and nothing in a per-subject tool\n * can say whether the narrow one is still earning its place — that question is\n * about the *set*, and no single subject knows it is a member of one.\n *\n * The two decisions this enables are opposites, and which one applies is a\n * judgement the data cannot make:\n *\n * - A component covered by many subjects may not need its own. Or the broad\n * subjects may be the ones that should stop looking at it, so that a change\n * inside it is reported once rather than a dozen times.\n * - A component covered by exactly one subject is the opposite risk. Deleting\n * that subject removes the only thing watching it, and nothing else in the\n * suite will notice when it breaks.\n *\n * **This never decides anything.** A component rendered in two contexts can be\n * correct in one and broken in the other, which is the entire reason contexts are\n * captured separately — so overlap is not redundancy, and a tool that deleted\n * subjects on this signal would remove real coverage on the strength of a name\n * matching. What is produced here is the list a person needs in order to make\n * that call, and the honest form of it is a count next to a name.\n *\n * Cheap by construction: it reads the component hashes a run already produced\n * (see {@link ./component-hash.js}) and touches no DOM, no browser and no image.\n *\n * ## What `composeSubjects` took from this, and what it left\n *\n * {@link ./composition.js} answers the same census question from richer input —\n * instances with provenance rather than hashes — and it is the one a run report\n * carries, so a component's subjects and instance count are now computed twice\n * from two inputs. If they ever disagree, `composeSubjects` is the one that is\n * right: it is what `variance run` writes and what every reader downstream of\n * the artifact is reading. `sole` there is `subjects.length === 1`.\n *\n * What survives here and nowhere else is {@link SubjectValue.unique} — the\n * components *no other subject in the set covers*. That is a question about a\n * subject rather than about a component, and composing answers the other one:\n * `examples` names the narrowest subject watching a component, which is not the\n * same as naming the only one. A suite pruning its own captures needs this; a\n * run explaining a diff needs that.\n */\n\n/** What one subject was found to contain. */\nexport interface SubjectCoverage {\n readonly subject: string;\n readonly components: readonly ComponentHash[];\n}\n\nexport interface ComponentCoverage {\n readonly component: string;\n\n /** Subjects containing at least one boundary of it, in the order supplied. */\n readonly subjects: readonly string[];\n\n /** Boundaries summed across every subject. Distinct from `subjects.length`. */\n readonly instances: number;\n\n /**\n * `true` when exactly one subject covers this component.\n *\n * The signal that a subject must not be deleted casually, rather than a\n * complaint. Sole coverage is normal and often correct.\n */\n readonly sole: boolean;\n}\n\nexport interface SubjectValue {\n readonly subject: string;\n readonly components: number;\n\n /**\n * Components no other subject in the set covers.\n *\n * Empty does **not** mean the subject is redundant — it means every component\n * it contains is watched somewhere else *as a component*, which says nothing\n * about whether this arrangement of them renders correctly. It is the\n * beginning of the question, not the answer.\n */\n readonly unique: readonly string[];\n}\n\nexport interface Coverage {\n /** Sorted by how many subjects cover each, widest first, then by name. */\n readonly components: readonly ComponentCoverage[];\n /** In the order the subjects were supplied. */\n readonly subjects: readonly SubjectValue[];\n}\n\nexport function coverageOf(subjects: readonly SubjectCoverage[]): Coverage {\n const byComponent = new Map<string, { subjects: string[]; instances: number }>();\n\n for (const subject of subjects) {\n // A subject that names the same component twice would otherwise inflate its\n // own coverage count. Boundaries are already folded per component upstream,\n // but this function is public and takes whatever it is handed.\n const seen = new Set<string>();\n\n for (const hash of subject.components) {\n const entry = byComponent.get(hash.component) ?? { subjects: [], instances: 0 };\n if (!seen.has(hash.component)) {\n entry.subjects.push(subject.subject);\n seen.add(hash.component);\n }\n entry.instances += hash.instances;\n byComponent.set(hash.component, entry);\n }\n }\n\n const components = [...byComponent.entries()]\n .map(([component, entry]) => ({\n component,\n subjects: entry.subjects,\n instances: entry.instances,\n sole: entry.subjects.length === 1,\n }))\n .sort(\n (a, b) =>\n b.subjects.length - a.subjects.length ||\n (a.component < b.component ? -1 : a.component > b.component ? 1 : 0),\n );\n\n const soleOwner = new Map<string, string>();\n for (const component of components) {\n if (component.sole) soleOwner.set(component.component, component.subjects[0]!);\n }\n\n return {\n components,\n subjects: subjects.map((subject) => {\n const names = [...new Set(subject.components.map((hash) => hash.component))];\n return {\n subject: subject.subject,\n components: names.length,\n unique: names.filter((name) => soleOwner.get(name) === subject.subject),\n };\n }),\n };\n}\n\n/** Subjects that would still watch this component if one of them were removed. */\nexport function alsoCovering(\n coverage: Coverage,\n component: string,\n excluding: string,\n): readonly string[] {\n const entry = coverage.components.find((candidate) => candidate.component === component);\n return entry === undefined ? [] : entry.subjects.filter((subject) => subject !== excluding);\n}\n\n/**\n * The overlap as something a person reads before deciding.\n *\n * Leads with sole coverage rather than with the widest components, because the\n * widest components are the ones a reader already knows about — they are in\n * every screenshot — while a component watched by exactly one subject is the fact\n * nobody has.\n */\nexport function summarizeCoverage(\n coverage: Coverage,\n options: { readonly limit?: number } = {},\n): string {\n const limit = options.limit ?? 10;\n\n if (coverage.components.length === 0) return 'no components covered';\n\n const sole = coverage.components.filter((component) => component.sole);\n const broad = coverage.components.filter((component) => component.subjects.length > 1);\n const deletable = coverage.subjects.filter((subject) => subject.unique.length === 0);\n\n const lines = [\n `${coverage.components.length} component(s) across ${coverage.subjects.length} subject(s)`,\n '',\n `${sole.length} component(s) covered by exactly one subject:`,\n ...sole.slice(0, limit).map((c) => ` ${c.component} — only in ${c.subjects[0]}`),\n ...(sole.length > limit ? [` +${sole.length - limit} more not listed`] : []),\n ];\n\n if (broad.length > 0) {\n lines.push(\n '',\n `${broad.length} component(s) covered by more than one subject, widest first:`,\n ...broad\n .slice(0, limit)\n .map((c) => ` ${c.component} — ${c.subjects.length} subject(s), ${c.instances} instance(s)`),\n ...(broad.length > limit ? [` +${broad.length - limit} more not listed`] : []),\n );\n }\n\n if (deletable.length > 0) {\n lines.push(\n '',\n `${deletable.length} subject(s) cover no component alone: ` +\n deletable\n .slice(0, limit)\n .map((s) => s.subject)\n .join(', ') +\n (deletable.length > limit ? `, +${deletable.length - limit} more` : ''),\n 'That is not a recommendation to delete them. A component can render correctly',\n 'in one context and wrongly in another, which is why the contexts are separate',\n 'subjects in the first place.',\n );\n }\n\n return lines.join('\\n');\n}\n"]}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Band } from '../compare/band.js';
|
|
2
|
+
import type { Digest } from '../format/hash.js';
|
|
3
|
+
import type { SemanticSnapshot } from '../format/snapshot.js';
|
|
4
|
+
import type { ComponentEntry, Rendering } from './composition.js';
|
|
5
|
+
/**
|
|
6
|
+
* The finding with no baseline in it, and the sentence that makes it actionable.
|
|
7
|
+
*
|
|
8
|
+
* Split out of `composition.ts` because it is the only part of the graph that
|
|
9
|
+
* reads *documents*. Everything else there folds digests a run already computed;
|
|
10
|
+
* this lifts two instances out of the pages they were found in and compares them
|
|
11
|
+
* — which is the only way to say `color`, and the difference between a reader
|
|
12
|
+
* knowing which subject to open and knowing what is waiting there.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The same component, the same inputs, and more than one rendering. At one commit.
|
|
16
|
+
*
|
|
17
|
+
* Not a regression and not a comparison — there is no baseline anywhere in it.
|
|
18
|
+
* It is a statement that the component's own inputs do not determine its output,
|
|
19
|
+
* which is either a fact about the design (a token, a theme, an ancestor) or the
|
|
20
|
+
* reading not being repeatable. `bands` says which kind of difference it is, in
|
|
21
|
+
* the same vocabulary a sensitivity absorbs, so a divergence entirely inside a
|
|
22
|
+
* band the subject relaxes is one a reader can dismiss without opening it.
|
|
23
|
+
*
|
|
24
|
+
* *Inputs*, not *props*, and the difference is the whole finding: a props digest
|
|
25
|
+
* excludes `children` and the checks in `divergencesOf` are what close the gap.
|
|
26
|
+
* A suite can legitimately produce none of these — `examples/todomvc` produces
|
|
27
|
+
* exactly zero — and that is the correct answer for a suite in which nothing
|
|
28
|
+
* renders two ways from one input, not a section to be filled.
|
|
29
|
+
*/
|
|
30
|
+
export interface Divergence {
|
|
31
|
+
readonly component: string;
|
|
32
|
+
readonly props?: Digest;
|
|
33
|
+
readonly bands: readonly Band[];
|
|
34
|
+
/** At least two, sorted by how many sites each has, widest first. */
|
|
35
|
+
readonly renderings: readonly Rendering[];
|
|
36
|
+
/**
|
|
37
|
+
* Why each rendering after the first parted from it.
|
|
38
|
+
*
|
|
39
|
+
* The half of this finding that used to be missing. "`Price` rendered two ways
|
|
40
|
+
* from one props digest" states the contradiction and stops there, leaving the
|
|
41
|
+
* reader to open two subjects and diff them by eye — which is the work the
|
|
42
|
+
* component graph was built to remove. These lines name the input: an
|
|
43
|
+
* ancestor's `color`, a context, a hook cell, or nothing readable at all.
|
|
44
|
+
*
|
|
45
|
+
* It is reachable here and nowhere else in the system, because a parting needs
|
|
46
|
+
* both sides read the same way and this is the only comparison where that is
|
|
47
|
+
* true by construction: both renderings come out of one run, off one collector,
|
|
48
|
+
* at one commit. A parting across two revisions has to reconcile two configs
|
|
49
|
+
* and two baselines first.
|
|
50
|
+
*
|
|
51
|
+
* Absent when the run supplied no snapshots to read — unknown, not "nothing to
|
|
52
|
+
* say" (ADR-0002). Shorter than `renderings.length - 1` when some rendering's
|
|
53
|
+
* site could not be lifted.
|
|
54
|
+
*/
|
|
55
|
+
readonly partings?: readonly DivergenceParting[];
|
|
56
|
+
}
|
|
57
|
+
/** One rendering of a divergence, and why it parted from the first. */
|
|
58
|
+
export interface DivergenceParting {
|
|
59
|
+
/** Index into {@link Divergence.renderings}, always at least 1. */
|
|
60
|
+
readonly rendering: number;
|
|
61
|
+
/** `explainParting` output, comparing that rendering against the first. */
|
|
62
|
+
readonly lines: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Every props class that produced more than one rendering *from one input*.
|
|
66
|
+
*
|
|
67
|
+
* Three refusals, and all three are the same refusal: a props digest is not a
|
|
68
|
+
* complete statement of a component's inputs, so most pairs of renderings that
|
|
69
|
+
* share one are not a contradiction. Measured on `examples/todomvc` before the
|
|
70
|
+
* checks below existed, **eleven divergences were reported and all eleven were
|
|
71
|
+
* false** — which is what a finding built on an incomplete key looks like.
|
|
72
|
+
*
|
|
73
|
+
* **Unknown props are not shared props.** Instances whose provenance did not
|
|
74
|
+
* survive are not known to have received the same thing. Grouping them and
|
|
75
|
+
* reporting that they render differently manufactures a finding out of missing
|
|
76
|
+
* data, in a system where absent must never read as equal.
|
|
77
|
+
*
|
|
78
|
+
* **Renderings that co-occur in one subject are not alternatives.** A component
|
|
79
|
+
* whose nodes are interrupted by a nested boundary is walked as two boundaries
|
|
80
|
+
* with one owner frame, so one `TextField` becomes a label-shaped rendering and
|
|
81
|
+
* an input-shaped one under a single props digest. That is one instance in two
|
|
82
|
+
* pieces, and it is indistinguishable from two instances that genuinely disagree
|
|
83
|
+
* — so it is not reported. A contradiction is a component that renders as A
|
|
84
|
+
* *here* and as B *there*, never both at once.
|
|
85
|
+
*
|
|
86
|
+
* **Different children are different inputs.** `propsDigest` excludes `children`
|
|
87
|
+
* deliberately (see `digestableProps` in `@variance-authority/react`: folding the
|
|
88
|
+
* subtree in would make every ancestor's props move on any descendant edit, and
|
|
89
|
+
* §6.2's root/collateral rule could never fire). The consequence is that
|
|
90
|
+
* `<Card><Stack/></Card>` and `<Card><Text/></Card>` share a props digest, and
|
|
91
|
+
* calling their different output a contradiction blames the component for its
|
|
92
|
+
* caller. Two proxies for "the children differed" are available and both are
|
|
93
|
+
* required to be quiet: the child components mounted, and the boundary's own
|
|
94
|
+
* text — which is where a string child lands.
|
|
95
|
+
*
|
|
96
|
+
* What survives is narrow on purpose. A movement wrongly dismissed as
|
|
97
|
+
* `contradicted` is an explanation nobody can act on; the same movement left
|
|
98
|
+
* unexplained lands on the suspect shortlist, where a second reading settles it.
|
|
99
|
+
* The asymmetry is the whole reason these checks are here rather than in prose.
|
|
100
|
+
*/
|
|
101
|
+
export declare function divergencesOf(components: readonly ComponentEntry[], snapshots: ReadonlyMap<string, SemanticSnapshot>): readonly Divergence[];
|
|
102
|
+
//# sourceMappingURL=divergence.d.ts.map
|