@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,244 @@
|
|
|
1
|
+
import { loudestBand } from '../band.js';
|
|
2
|
+
import { aggregateImpact } from '../impact.js';
|
|
3
|
+
import { impactTag } from './delta.js';
|
|
4
|
+
/**
|
|
5
|
+
* Grouping deltas into explanations — the docket's entire value proposition.
|
|
6
|
+
*
|
|
7
|
+
* Separated from `index.ts` because it is the half of diffing that has nothing
|
|
8
|
+
* to do with observation: it takes deltas that already exist and decides which
|
|
9
|
+
* *one* sentence covers them. The precedence rules, the metric-delta folding and
|
|
10
|
+
* the boundary walk are all in service of the same claim — report the largest
|
|
11
|
+
* explanation once rather than the smallest one many times — and they only make
|
|
12
|
+
* sense read together.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Group deltas into explanations (spec §6.2).
|
|
16
|
+
*
|
|
17
|
+
* Precedence is fixed and deliberate: environment, then token, then props, then
|
|
18
|
+
* component. Each level explains more subjects than the one below it, and the
|
|
19
|
+
* docket's value is entirely in reporting the *largest* explanation once rather
|
|
20
|
+
* than the smallest one many times.
|
|
21
|
+
*/
|
|
22
|
+
export function attribute(deltas, matching, environmentDeltas) {
|
|
23
|
+
const groups = new Map();
|
|
24
|
+
const add = (id, kind, label, delta, cause) => {
|
|
25
|
+
const existing = groups.get(id);
|
|
26
|
+
if (existing)
|
|
27
|
+
existing.deltas.push(delta);
|
|
28
|
+
else
|
|
29
|
+
groups.set(id, { kind, label, deltas: [delta], ...(cause !== undefined ? { cause } : {}) });
|
|
30
|
+
};
|
|
31
|
+
// A changed environment explains everything under it, so it is collapsed to a
|
|
32
|
+
// single root — the one-action re-baselining case of spec §7.3.
|
|
33
|
+
if (environmentDeltas.length > 0) {
|
|
34
|
+
for (const environmentDelta of environmentDeltas) {
|
|
35
|
+
groups.set(`env:${environmentDelta.field}`, {
|
|
36
|
+
kind: 'environment',
|
|
37
|
+
label: `${environmentDelta.field} ${environmentDelta.from} → ${environmentDelta.to}`,
|
|
38
|
+
deltas: [],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const propsBefore = ownerDigests(matching, 'before');
|
|
43
|
+
const propsAfter = ownerDigests(matching, 'after');
|
|
44
|
+
const classify = (delta) => {
|
|
45
|
+
if (delta.token)
|
|
46
|
+
return { id: `token:${delta.token}`, kind: 'token', label: delta.token };
|
|
47
|
+
const owners = delta.owners;
|
|
48
|
+
if (owners === undefined || owners.length === 0)
|
|
49
|
+
return null;
|
|
50
|
+
const boundary = changedBoundary(owners, propsBefore, propsAfter);
|
|
51
|
+
if (boundary === null) {
|
|
52
|
+
// Every incoming props digest held, so the change originated inside a
|
|
53
|
+
// component rather than arriving from outside.
|
|
54
|
+
//
|
|
55
|
+
// *Which* component depends on what kind of change it is. A style change
|
|
56
|
+
// belongs to the component the styled node sits in. A node appearing,
|
|
57
|
+
// vanishing, or moving belongs to whoever *decided it is there* — the
|
|
58
|
+
// component whose JSX created the element, not the component the element
|
|
59
|
+
// is. When a filter list reorders, the nodes that moved are `Chip`s
|
|
60
|
+
// enclosed by a `Stack`, and naming either reports the thing that was
|
|
61
|
+
// rearranged instead of the code that rearranged it.
|
|
62
|
+
const owner = owners[0];
|
|
63
|
+
// A node that *moved* is the only case where the responsible component is
|
|
64
|
+
// not the one the node belongs to. Ordering is decided by whoever wrote the
|
|
65
|
+
// JSX that placed the element, so a reordered filter list is `TodoFooter`'s
|
|
66
|
+
// change even though every node that moved is a `Chip`.
|
|
67
|
+
//
|
|
68
|
+
// Appearing and disappearing are deliberately *not* treated this way. A
|
|
69
|
+
// component that swaps its own output — `Toggle` rendering a `<div>` where
|
|
70
|
+
// it used to render an `<input>` — produces an added and a removed node
|
|
71
|
+
// whose creator is `Toggle` itself, and crediting whoever placed `<Toggle>`
|
|
72
|
+
// would blame `TodoItem` for a change it did not make.
|
|
73
|
+
//
|
|
74
|
+
// Known cost: when a component element is added or removed wholesale, this
|
|
75
|
+
// names the component that appeared rather than the one that decided to
|
|
76
|
+
// render it. Distinguishing those needs to know whether the component still
|
|
77
|
+
// exists on the other side, which is a question about the change set rather
|
|
78
|
+
// than about the delta.
|
|
79
|
+
const name = (delta.kind === 'node-moved' ? owner.createdBy : undefined) ?? owner.name;
|
|
80
|
+
return { id: `component:${name}`, kind: 'component', label: name, cause: name };
|
|
81
|
+
}
|
|
82
|
+
// Props moved at a boundary, so the change arrived from outside. The root is
|
|
83
|
+
// the provider — the component that passes props across that boundary.
|
|
84
|
+
const changed = owners[boundary];
|
|
85
|
+
const provider = owners[boundary + 1];
|
|
86
|
+
return {
|
|
87
|
+
id: `prop:${provider?.name ?? '?'}>${changed.name}`,
|
|
88
|
+
kind: 'prop',
|
|
89
|
+
label: provider ? `${provider.name} → ${changed.name}` : changed.name,
|
|
90
|
+
// The *provider*, not the component the delta landed in. This is the whole
|
|
91
|
+
// point of a `prop` root: the edit is upstream, and the report has to send
|
|
92
|
+
// a reviewer there.
|
|
93
|
+
//
|
|
94
|
+
// Set **only when a provider exists**. With none, the changed boundary is
|
|
95
|
+
// the outermost frame in the chain: the props arrived from outside the
|
|
96
|
+
// subject entirely, and no component inside it is responsible. Naming the
|
|
97
|
+
// boundary there would send a reviewer to a component whose source is also
|
|
98
|
+
// unchanged, which is the same failure one level out. The fallback in
|
|
99
|
+
// `componentsOf` — the innermost owner, where the change landed — is what
|
|
100
|
+
// both corpora ask for in that case, and the entry's own label still says
|
|
101
|
+
// whose props moved.
|
|
102
|
+
...(provider !== undefined ? { cause: provider.name } : {}),
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
// Metric deltas are held back until every cause has a group, then attached to
|
|
106
|
+
// the nearest one. Without this a profile with layout reports a root per
|
|
107
|
+
// component whose box happened to resize — the change is not lost, but "one
|
|
108
|
+
// root plus counted collateral" becomes "one root per affected component",
|
|
109
|
+
// which is the report spec §6.2 exists to prevent.
|
|
110
|
+
const causes = deltas.filter((delta) => !isMetric(delta)).map((delta) => ({ delta, group: classify(delta) }));
|
|
111
|
+
for (const { delta, group } of causes) {
|
|
112
|
+
if (group === null)
|
|
113
|
+
add('unattributed', 'unattributed', 'no owner chain', delta);
|
|
114
|
+
else
|
|
115
|
+
add(group.id, group.kind, group.label, delta, group.cause);
|
|
116
|
+
}
|
|
117
|
+
for (const delta of deltas) {
|
|
118
|
+
if (!isMetric(delta))
|
|
119
|
+
continue;
|
|
120
|
+
const target = nearestCause(delta, causes);
|
|
121
|
+
if (target !== undefined) {
|
|
122
|
+
add(target.id, target.kind, target.label, delta, target.cause);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// No cause anywhere: the box moved and nothing explains it. That is a real
|
|
126
|
+
// finding — most often a change outside the subject reaching in — and it
|
|
127
|
+
// keeps its own root rather than being attached to an unrelated one.
|
|
128
|
+
const own = classify(delta);
|
|
129
|
+
if (own === null)
|
|
130
|
+
add('unattributed', 'unattributed', 'no owner chain', delta);
|
|
131
|
+
else
|
|
132
|
+
add(own.id, own.kind, own.label, delta, own.cause);
|
|
133
|
+
}
|
|
134
|
+
return [...groups.entries()].map(([id, group]) => ({
|
|
135
|
+
id,
|
|
136
|
+
kind: group.kind,
|
|
137
|
+
label: group.label,
|
|
138
|
+
...(group.cause !== undefined ? { cause: group.cause } : {}),
|
|
139
|
+
band: dominantBand(group.deltas),
|
|
140
|
+
impact: aggregateImpact(group.deltas.map(impactTag)),
|
|
141
|
+
deltas: group.deltas,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Computed properties that are functions of the used box rather than of any
|
|
146
|
+
* declaration.
|
|
147
|
+
*
|
|
148
|
+
* A layout engine reports these for every element whether or not a stylesheet
|
|
149
|
+
* mentioned them, so they move whenever anything inside or above the node
|
|
150
|
+
* changes size. Treating them as causes is what turned one token edit into five
|
|
151
|
+
* docket entries under `chromium` — `band.ts` already says the attributor must
|
|
152
|
+
* fold rect movement into the style change as collateral, and these are the
|
|
153
|
+
* `style-changed` half of the same evidence.
|
|
154
|
+
*
|
|
155
|
+
* Note the cost of the simplification: a genuine `width: 100px → 200px` edit is
|
|
156
|
+
* folded too, whenever the same node carries another delta to fold into. That
|
|
157
|
+
* loses no delta and moves no verdict — the change is still reported, under a
|
|
158
|
+
* root that is a strictly better description of the cause.
|
|
159
|
+
*/
|
|
160
|
+
const USED_VALUE_PROPERTIES = new Set([
|
|
161
|
+
'width', 'height', 'inline-size', 'block-size',
|
|
162
|
+
'transform-origin', 'perspective-origin',
|
|
163
|
+
]);
|
|
164
|
+
/** Evidence that a box moved, as opposed to evidence of why. */
|
|
165
|
+
function isMetric(delta) {
|
|
166
|
+
if (delta.kind === 'rect-changed')
|
|
167
|
+
return true;
|
|
168
|
+
return delta.kind === 'style-changed' && USED_VALUE_PROPERTIES.has(delta.property ?? '');
|
|
169
|
+
}
|
|
170
|
+
/** Paths are `/`-joined child indices, so containment is a prefix test. */
|
|
171
|
+
function isDescendant(candidate, ancestor) {
|
|
172
|
+
return candidate.startsWith(`${ancestor}/`);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The cause a metric delta belongs to: same node first, then the nearest change
|
|
176
|
+
* *inside* it, then the nearest change above it.
|
|
177
|
+
*
|
|
178
|
+
* Inside before above, because a box that grew did so because of its contents
|
|
179
|
+
* far more often than because of its container — and when a container really is
|
|
180
|
+
* the cause, its own metric delta folds upward on the same rule, so the two
|
|
181
|
+
* meet at the same root either way.
|
|
182
|
+
*/
|
|
183
|
+
function nearestCause(delta, causes) {
|
|
184
|
+
const grouped = causes.filter((cause) => cause.group !== null);
|
|
185
|
+
const here = grouped.find((cause) => cause.delta.path === delta.path);
|
|
186
|
+
if (here)
|
|
187
|
+
return here.group ?? undefined;
|
|
188
|
+
const inside = grouped
|
|
189
|
+
.filter((cause) => isDescendant(cause.delta.path, delta.path))
|
|
190
|
+
.sort((a, b) => a.delta.path.length - b.delta.path.length)[0];
|
|
191
|
+
if (inside)
|
|
192
|
+
return inside.group ?? undefined;
|
|
193
|
+
const above = grouped
|
|
194
|
+
.filter((cause) => isDescendant(delta.path, cause.delta.path))
|
|
195
|
+
.sort((a, b) => b.delta.path.length - a.delta.path.length)[0];
|
|
196
|
+
return above?.group ?? undefined;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The outermost owner whose incoming props changed, or `null` if none did.
|
|
200
|
+
*
|
|
201
|
+
* Outermost rather than innermost: if `CheckoutPage` passes a new prop to
|
|
202
|
+
* `Header`, which passes it to `Button`, all three boundaries moved, and the
|
|
203
|
+
* useful root is the one furthest up — reporting `Button` would name the
|
|
204
|
+
* messenger.
|
|
205
|
+
*/
|
|
206
|
+
function changedBoundary(owners, before, after) {
|
|
207
|
+
let outermost = null;
|
|
208
|
+
for (let index = 0; index < owners.length; index += 1) {
|
|
209
|
+
const owner = owners[index];
|
|
210
|
+
const digestBefore = before.get(owner.name);
|
|
211
|
+
// An owner absent from the baseline is new structure, not a prop change;
|
|
212
|
+
// the structural delta covers it and inventing a prop root would double-count.
|
|
213
|
+
if (digestBefore === undefined)
|
|
214
|
+
continue;
|
|
215
|
+
if (digestBefore !== after.get(owner.name))
|
|
216
|
+
outermost = index;
|
|
217
|
+
}
|
|
218
|
+
return outermost;
|
|
219
|
+
}
|
|
220
|
+
function ownerDigests(matching, side) {
|
|
221
|
+
const digests = new Map();
|
|
222
|
+
for (const [before, after] of matching.pairs) {
|
|
223
|
+
const node = side === 'before' ? before : after;
|
|
224
|
+
for (const owner of node.provenance?.owners ?? []) {
|
|
225
|
+
// First occurrence wins: a component rendered many times with different
|
|
226
|
+
// props is not the same boundary, and this map answers the coarser
|
|
227
|
+
// question of whether *anything* at that boundary moved.
|
|
228
|
+
if (!digests.has(owner.name))
|
|
229
|
+
digests.set(owner.name, owner.propsDigest);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return digests;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Loudest band present. A group containing one `a11y` delta is an `a11y` root,
|
|
236
|
+
* whatever else moved alongside it.
|
|
237
|
+
*
|
|
238
|
+
* `texture` is the fallback for the empty case rather than a claim: a root is
|
|
239
|
+
* only built from deltas, so the set is never actually empty here.
|
|
240
|
+
*/
|
|
241
|
+
function dominantBand(deltas) {
|
|
242
|
+
return loudestBand(deltas.map((delta) => delta.band)) ?? 'texture';
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=attribution.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attribution.js","sourceRoot":"","sources":["../../../src/compare/diff/attribution.ts"],"names":[],"mappings":"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","sourcesContent":["import { loudestBand, type Band } from '../band.js';\nimport { aggregateImpact } from '../impact.js';\nimport type { EnvironmentDelta } from '../../format/environment.js';\nimport type { OwnerFrame } from '../../format/provenance.js';\nimport { impactTag, type Delta, type Root, type RootKind } from './delta.js';\nimport type { Matching } from './match.js';\n\n/**\n * Grouping deltas into explanations — the docket's entire value proposition.\n *\n * Separated from `index.ts` because it is the half of diffing that has nothing\n * to do with observation: it takes deltas that already exist and decides which\n * *one* sentence covers them. The precedence rules, the metric-delta folding and\n * the boundary walk are all in service of the same claim — report the largest\n * explanation once rather than the smallest one many times — and they only make\n * sense read together.\n */\n\n/**\n * Group deltas into explanations (spec §6.2).\n *\n * Precedence is fixed and deliberate: environment, then token, then props, then\n * component. Each level explains more subjects than the one below it, and the\n * docket's value is entirely in reporting the *largest* explanation once rather\n * than the smallest one many times.\n */\nexport function attribute(\n deltas: readonly Delta[],\n matching: Matching,\n environmentDeltas: readonly EnvironmentDelta[],\n): readonly Root[] {\n const groups = new Map<string, { kind: RootKind; label: string; cause?: string; deltas: Delta[] }>();\n\n const add = (id: string, kind: RootKind, label: string, delta: Delta, cause?: string): void => {\n const existing = groups.get(id);\n if (existing) existing.deltas.push(delta);\n else groups.set(id, { kind, label, deltas: [delta], ...(cause !== undefined ? { cause } : {}) });\n };\n\n // A changed environment explains everything under it, so it is collapsed to a\n // single root — the one-action re-baselining case of spec §7.3.\n if (environmentDeltas.length > 0) {\n for (const environmentDelta of environmentDeltas) {\n groups.set(`env:${environmentDelta.field}`, {\n kind: 'environment',\n label: `${environmentDelta.field} ${environmentDelta.from} → ${environmentDelta.to}`,\n deltas: [],\n });\n }\n }\n\n const propsBefore = ownerDigests(matching, 'before');\n const propsAfter = ownerDigests(matching, 'after');\n\n const classify = (delta: Delta): Group | null => {\n if (delta.token) return { id: `token:${delta.token}`, kind: 'token', label: delta.token };\n\n const owners = delta.owners;\n if (owners === undefined || owners.length === 0) return null;\n\n const boundary = changedBoundary(owners, propsBefore, propsAfter);\n\n if (boundary === null) {\n // Every incoming props digest held, so the change originated inside a\n // component rather than arriving from outside.\n //\n // *Which* component depends on what kind of change it is. A style change\n // belongs to the component the styled node sits in. A node appearing,\n // vanishing, or moving belongs to whoever *decided it is there* — the\n // component whose JSX created the element, not the component the element\n // is. When a filter list reorders, the nodes that moved are `Chip`s\n // enclosed by a `Stack`, and naming either reports the thing that was\n // rearranged instead of the code that rearranged it.\n const owner = owners[0]!;\n\n // A node that *moved* is the only case where the responsible component is\n // not the one the node belongs to. Ordering is decided by whoever wrote the\n // JSX that placed the element, so a reordered filter list is `TodoFooter`'s\n // change even though every node that moved is a `Chip`.\n //\n // Appearing and disappearing are deliberately *not* treated this way. A\n // component that swaps its own output — `Toggle` rendering a `<div>` where\n // it used to render an `<input>` — produces an added and a removed node\n // whose creator is `Toggle` itself, and crediting whoever placed `<Toggle>`\n // would blame `TodoItem` for a change it did not make.\n //\n // Known cost: when a component element is added or removed wholesale, this\n // names the component that appeared rather than the one that decided to\n // render it. Distinguishing those needs to know whether the component still\n // exists on the other side, which is a question about the change set rather\n // than about the delta.\n const name = (delta.kind === 'node-moved' ? owner.createdBy : undefined) ?? owner.name;\n\n return { id: `component:${name}`, kind: 'component', label: name, cause: name };\n }\n\n // Props moved at a boundary, so the change arrived from outside. The root is\n // the provider — the component that passes props across that boundary.\n const changed = owners[boundary]!;\n const provider = owners[boundary + 1];\n return {\n id: `prop:${provider?.name ?? '?'}>${changed.name}`,\n kind: 'prop',\n label: provider ? `${provider.name} → ${changed.name}` : changed.name,\n // The *provider*, not the component the delta landed in. This is the whole\n // point of a `prop` root: the edit is upstream, and the report has to send\n // a reviewer there.\n //\n // Set **only when a provider exists**. With none, the changed boundary is\n // the outermost frame in the chain: the props arrived from outside the\n // subject entirely, and no component inside it is responsible. Naming the\n // boundary there would send a reviewer to a component whose source is also\n // unchanged, which is the same failure one level out. The fallback in\n // `componentsOf` — the innermost owner, where the change landed — is what\n // both corpora ask for in that case, and the entry's own label still says\n // whose props moved.\n ...(provider !== undefined ? { cause: provider.name } : {}),\n };\n };\n\n // Metric deltas are held back until every cause has a group, then attached to\n // the nearest one. Without this a profile with layout reports a root per\n // component whose box happened to resize — the change is not lost, but \"one\n // root plus counted collateral\" becomes \"one root per affected component\",\n // which is the report spec §6.2 exists to prevent.\n const causes = deltas.filter((delta) => !isMetric(delta)).map((delta) => ({ delta, group: classify(delta) }));\n\n for (const { delta, group } of causes) {\n if (group === null) add('unattributed', 'unattributed', 'no owner chain', delta);\n else add(group.id, group.kind, group.label, delta, group.cause);\n }\n\n for (const delta of deltas) {\n if (!isMetric(delta)) continue;\n\n const target = nearestCause(delta, causes);\n\n if (target !== undefined) {\n add(target.id, target.kind, target.label, delta, target.cause);\n continue;\n }\n\n // No cause anywhere: the box moved and nothing explains it. That is a real\n // finding — most often a change outside the subject reaching in — and it\n // keeps its own root rather than being attached to an unrelated one.\n const own = classify(delta);\n if (own === null) add('unattributed', 'unattributed', 'no owner chain', delta);\n else add(own.id, own.kind, own.label, delta, own.cause);\n }\n\n return [...groups.entries()].map(([id, group]) => ({\n id,\n kind: group.kind,\n label: group.label,\n ...(group.cause !== undefined ? { cause: group.cause } : {}),\n band: dominantBand(group.deltas),\n impact: aggregateImpact(group.deltas.map(impactTag)),\n deltas: group.deltas,\n }));\n}\n\ninterface Group {\n readonly id: string;\n readonly kind: RootKind;\n readonly label: string;\n /** The component responsible. See `Root.cause`. */\n readonly cause?: string;\n}\n\n/**\n * Computed properties that are functions of the used box rather than of any\n * declaration.\n *\n * A layout engine reports these for every element whether or not a stylesheet\n * mentioned them, so they move whenever anything inside or above the node\n * changes size. Treating them as causes is what turned one token edit into five\n * docket entries under `chromium` — `band.ts` already says the attributor must\n * fold rect movement into the style change as collateral, and these are the\n * `style-changed` half of the same evidence.\n *\n * Note the cost of the simplification: a genuine `width: 100px → 200px` edit is\n * folded too, whenever the same node carries another delta to fold into. That\n * loses no delta and moves no verdict — the change is still reported, under a\n * root that is a strictly better description of the cause.\n */\nconst USED_VALUE_PROPERTIES: ReadonlySet<string> = new Set([\n 'width', 'height', 'inline-size', 'block-size',\n 'transform-origin', 'perspective-origin',\n]);\n\n/** Evidence that a box moved, as opposed to evidence of why. */\nfunction isMetric(delta: Delta): boolean {\n if (delta.kind === 'rect-changed') return true;\n return delta.kind === 'style-changed' && USED_VALUE_PROPERTIES.has(delta.property ?? '');\n}\n\n/** Paths are `/`-joined child indices, so containment is a prefix test. */\nfunction isDescendant(candidate: string, ancestor: string): boolean {\n return candidate.startsWith(`${ancestor}/`);\n}\n\n/**\n * The cause a metric delta belongs to: same node first, then the nearest change\n * *inside* it, then the nearest change above it.\n *\n * Inside before above, because a box that grew did so because of its contents\n * far more often than because of its container — and when a container really is\n * the cause, its own metric delta folds upward on the same rule, so the two\n * meet at the same root either way.\n */\nfunction nearestCause(\n delta: Delta,\n causes: readonly { readonly delta: Delta; readonly group: Group | null }[],\n): Group | undefined {\n const grouped = causes.filter((cause) => cause.group !== null);\n\n const here = grouped.find((cause) => cause.delta.path === delta.path);\n if (here) return here.group ?? undefined;\n\n const inside = grouped\n .filter((cause) => isDescendant(cause.delta.path, delta.path))\n .sort((a, b) => a.delta.path.length - b.delta.path.length)[0];\n if (inside) return inside.group ?? undefined;\n\n const above = grouped\n .filter((cause) => isDescendant(delta.path, cause.delta.path))\n .sort((a, b) => b.delta.path.length - a.delta.path.length)[0];\n\n return above?.group ?? undefined;\n}\n\n/**\n * The outermost owner whose incoming props changed, or `null` if none did.\n *\n * Outermost rather than innermost: if `CheckoutPage` passes a new prop to\n * `Header`, which passes it to `Button`, all three boundaries moved, and the\n * useful root is the one furthest up — reporting `Button` would name the\n * messenger.\n */\nfunction changedBoundary(\n owners: readonly OwnerFrame[],\n before: ReadonlyMap<string, string>,\n after: ReadonlyMap<string, string>,\n): number | null {\n let outermost: number | null = null;\n\n for (let index = 0; index < owners.length; index += 1) {\n const owner = owners[index]!;\n const digestBefore = before.get(owner.name);\n // An owner absent from the baseline is new structure, not a prop change;\n // the structural delta covers it and inventing a prop root would double-count.\n if (digestBefore === undefined) continue;\n if (digestBefore !== after.get(owner.name)) outermost = index;\n }\n\n return outermost;\n}\n\nfunction ownerDigests(\n matching: Matching,\n side: 'before' | 'after',\n): ReadonlyMap<string, string> {\n const digests = new Map<string, string>();\n\n for (const [before, after] of matching.pairs) {\n const node = side === 'before' ? before : after;\n for (const owner of node.provenance?.owners ?? []) {\n // First occurrence wins: a component rendered many times with different\n // props is not the same boundary, and this map answers the coarser\n // question of whether *anything* at that boundary moved.\n if (!digests.has(owner.name)) digests.set(owner.name, owner.propsDigest);\n }\n }\n\n return digests;\n}\n\n/**\n * Loudest band present. A group containing one `a11y` delta is an `a11y` root,\n * whatever else moved alongside it.\n *\n * `texture` is the fallback for the empty case rather than a claim: a root is\n * only built from deltas, so the set is never actually empty here.\n */\nfunction dominantBand(deltas: readonly Delta[]): Band {\n return loudestBand(deltas.map((delta) => delta.band)) ?? 'texture';\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SemanticNode } from '../../format/snapshot.js';
|
|
2
|
+
import type { Delta } from './delta.js';
|
|
3
|
+
/**
|
|
4
|
+
* Delta production for a single node, or a single matched pair of nodes.
|
|
5
|
+
*
|
|
6
|
+
* Separated from `index.ts` because everything here answers a strictly local
|
|
7
|
+
* question — given two nodes already known to be the same node, which fields
|
|
8
|
+
* moved — and knows nothing about trees, roots, or attribution. The token
|
|
9
|
+
* reasoning lives here rather than with the attributor for the same reason: the
|
|
10
|
+
* evidence that decides whether a property change is collateral of a token edit
|
|
11
|
+
* is the pair of nodes, and nothing further out has it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function wholeNode(kind: 'node-added' | 'node-removed' | 'node-moved', node: SemanticNode): Delta;
|
|
14
|
+
export declare function compareNodes(before: SemanticNode, after: SemanticNode, deltas: Delta[], hasLayout: boolean): void;
|
|
15
|
+
//# sourceMappingURL=compare-nodes.d.ts.map
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { bandOf } from '../band.js';
|
|
2
|
+
import { impactOf } from '../impact.js';
|
|
3
|
+
/**
|
|
4
|
+
* Delta production for a single node, or a single matched pair of nodes.
|
|
5
|
+
*
|
|
6
|
+
* Separated from `index.ts` because everything here answers a strictly local
|
|
7
|
+
* question — given two nodes already known to be the same node, which fields
|
|
8
|
+
* moved — and knows nothing about trees, roots, or attribution. The token
|
|
9
|
+
* reasoning lives here rather than with the attributor for the same reason: the
|
|
10
|
+
* evidence that decides whether a property change is collateral of a token edit
|
|
11
|
+
* is the pair of nodes, and nothing further out has it.
|
|
12
|
+
*/
|
|
13
|
+
export function wholeNode(kind, node) {
|
|
14
|
+
return {
|
|
15
|
+
kind,
|
|
16
|
+
band: bandOf(kind),
|
|
17
|
+
path: node.path,
|
|
18
|
+
to: describe(node),
|
|
19
|
+
...(node.provenance ? { owners: node.provenance.owners } : {}),
|
|
20
|
+
...(node.provenance?.createdBy ? { createdBy: node.provenance.createdBy } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function describe(node) {
|
|
24
|
+
if (node.role !== undefined && node.name !== undefined)
|
|
25
|
+
return `${node.role} "${node.name}"`;
|
|
26
|
+
if (node.role !== undefined)
|
|
27
|
+
return node.role;
|
|
28
|
+
if (node.text !== undefined)
|
|
29
|
+
return `${node.tag} "${node.text}"`;
|
|
30
|
+
return node.tag;
|
|
31
|
+
}
|
|
32
|
+
export function compareNodes(before, after, deltas, hasLayout) {
|
|
33
|
+
const owners = after.provenance?.owners;
|
|
34
|
+
const createdBy = after.provenance?.createdBy;
|
|
35
|
+
const base = (kind) => ({
|
|
36
|
+
kind,
|
|
37
|
+
band: bandOf(kind),
|
|
38
|
+
path: after.path,
|
|
39
|
+
...(owners ? { owners } : {}),
|
|
40
|
+
...(createdBy ? { createdBy } : {}),
|
|
41
|
+
});
|
|
42
|
+
if (before.role !== after.role) {
|
|
43
|
+
deltas.push({ ...base('role-changed'), from: before.role, to: after.role });
|
|
44
|
+
}
|
|
45
|
+
if (before.name !== after.name) {
|
|
46
|
+
deltas.push({ ...base('name-changed'), from: before.name, to: after.name });
|
|
47
|
+
}
|
|
48
|
+
if (before.description !== after.description) {
|
|
49
|
+
deltas.push({
|
|
50
|
+
...base('description-changed'),
|
|
51
|
+
from: before.description,
|
|
52
|
+
to: after.description,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (before.text !== after.text) {
|
|
56
|
+
deltas.push({ ...base('text-changed'), from: before.text, to: after.text });
|
|
57
|
+
}
|
|
58
|
+
for (const property of unionKeys(before.state, after.state)) {
|
|
59
|
+
const from = before.state?.[property];
|
|
60
|
+
const to = after.state?.[property];
|
|
61
|
+
if (from !== to) {
|
|
62
|
+
deltas.push({ ...base('state-changed'), property, from: str(from), to: str(to) });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const property of unionKeys(before.attributes, after.attributes)) {
|
|
66
|
+
const from = before.attributes[property];
|
|
67
|
+
const to = after.attributes[property];
|
|
68
|
+
if (from !== to) {
|
|
69
|
+
deltas.push({ ...base('attribute-changed'), property, from, to });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let reflowCause = null;
|
|
73
|
+
for (const property of unionKeys(before.style, after.style)) {
|
|
74
|
+
const from = before.style[property];
|
|
75
|
+
const to = after.style[property];
|
|
76
|
+
if (from === to)
|
|
77
|
+
continue;
|
|
78
|
+
// A token whose own value moved makes this delta collateral rather than a
|
|
79
|
+
// root. Recorded here so attribution does not have to re-derive it.
|
|
80
|
+
const token = changedTokenFor(before, after, property);
|
|
81
|
+
const impact = impactOf(property);
|
|
82
|
+
const delta = {
|
|
83
|
+
...base('style-changed'),
|
|
84
|
+
property,
|
|
85
|
+
from,
|
|
86
|
+
to,
|
|
87
|
+
impact,
|
|
88
|
+
...(token ? { token } : {}),
|
|
89
|
+
};
|
|
90
|
+
deltas.push(delta);
|
|
91
|
+
if (impact === 'layout' && reflowCause === null)
|
|
92
|
+
reflowCause = delta;
|
|
93
|
+
}
|
|
94
|
+
// Which token a property resolves *through* is part of the render hash, so a
|
|
95
|
+
// change in it moves the hash — but under a profile that cannot resolve custom
|
|
96
|
+
// properties both resolved values are the same empty string and the loop above
|
|
97
|
+
// sees nothing. The result was a non-identical diff carrying zero deltas and
|
|
98
|
+
// zero roots: a correct verdict with an empty docket, which tells a reviewer
|
|
99
|
+
// that something changed and then refuses to say what. Found by scoring
|
|
100
|
+
// `prop-size/button` under `jsdom`, which only became scorable with ADR-0008.
|
|
101
|
+
//
|
|
102
|
+
// Skipped when the resolved value moved too: the `style-changed` delta above
|
|
103
|
+
// already names that property, and reporting both splits one cause in two.
|
|
104
|
+
for (const property of unionKeys(before.styleTokens, after.styleTokens)) {
|
|
105
|
+
const from = before.styleTokens?.[property];
|
|
106
|
+
const to = after.styleTokens?.[property];
|
|
107
|
+
if (from === to)
|
|
108
|
+
continue;
|
|
109
|
+
if (before.style[property] !== after.style[property])
|
|
110
|
+
continue;
|
|
111
|
+
deltas.push({ ...base('token-changed'), property, from, to });
|
|
112
|
+
}
|
|
113
|
+
if (hasLayout && !sameRect(before, after)) {
|
|
114
|
+
// A rect that moved because this node's own padding changed is the same
|
|
115
|
+
// finding observed twice. Folding it under its cause — and inheriting that
|
|
116
|
+
// cause's token — keeps one edit as one docket entry instead of splitting it
|
|
117
|
+
// into a `token` root and an unrelated-looking `geometry` one.
|
|
118
|
+
//
|
|
119
|
+
// A rect that moved with *no* layout-impact change here is different and
|
|
120
|
+
// stays independent: something upstream reflowed and pushed this node, which
|
|
121
|
+
// is exactly the propagation worth surfacing.
|
|
122
|
+
deltas.push({
|
|
123
|
+
...base('rect-changed'),
|
|
124
|
+
...(before.rect ? { rectFrom: before.rect } : {}),
|
|
125
|
+
...(after.rect ? { rectTo: after.rect } : {}),
|
|
126
|
+
...(reflowCause
|
|
127
|
+
? {
|
|
128
|
+
derivedFrom: `${reflowCause.path}:${reflowCause.property ?? ''}`,
|
|
129
|
+
...(reflowCause.token ? { token: reflowCause.token } : {}),
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The token that explains *this property's* change, if one does.
|
|
137
|
+
*
|
|
138
|
+
* Two conditions, and both are load-bearing. The property must actually resolve
|
|
139
|
+
* through the token — a node whose `color` comes from a token and whose padding
|
|
140
|
+
* comes from a literal is collateral of a token edit only in its colour. And the
|
|
141
|
+
* token's own value must have moved: a node that merely mentions
|
|
142
|
+
* `--color-primary` while that token held is not collateral of anything, its own
|
|
143
|
+
* rule changed, and it is a root.
|
|
144
|
+
*
|
|
145
|
+
* Dropping either check splits one docket entry into several. Ignoring the
|
|
146
|
+
* property meant a rule that overrode a token-driven value got attributed to the
|
|
147
|
+
* token it had just stopped using.
|
|
148
|
+
*/
|
|
149
|
+
function changedTokenFor(before, after, property) {
|
|
150
|
+
const token = before.styleTokens?.[property];
|
|
151
|
+
// The *same* token must drive the property on both sides. A property that
|
|
152
|
+
// stopped resolving through a token did so because some rule started winning
|
|
153
|
+
// instead — that rule is the root, and blaming the abandoned token would name
|
|
154
|
+
// the thing that did not change.
|
|
155
|
+
// Properties whose initial value is `currentColor` follow `color` without ever
|
|
156
|
+
// naming it: no rule declares `outline-color`, so it resolves through no token,
|
|
157
|
+
// yet a token-driven `color` change moves it. Under a profile with computed
|
|
158
|
+
// style that produced a second root per Button — "the accent token moved" and
|
|
159
|
+
// "Button changed" — for one edit. Recognised by the value matching `color` on
|
|
160
|
+
// both sides, so a node that genuinely declares its own outline colour is
|
|
161
|
+
// unaffected.
|
|
162
|
+
if (token === undefined && FOLLOWS_CURRENT_COLOR.has(property)) {
|
|
163
|
+
const from = before.style[property];
|
|
164
|
+
const to = after.style[property];
|
|
165
|
+
if (from !== undefined && to !== undefined && from === before.style['color'] && to === after.style['color']) {
|
|
166
|
+
return changedTokenFor(before, after, 'color');
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
if (token === undefined || after.styleTokens?.[property] !== token)
|
|
171
|
+
return undefined;
|
|
172
|
+
// Undefined on one side counts as a change: a theme override introducing a
|
|
173
|
+
// token that previously had no value is exactly the case the token band exists
|
|
174
|
+
// to collapse into one root.
|
|
175
|
+
if (before.tokens?.[token] !== after.tokens?.[token])
|
|
176
|
+
return token;
|
|
177
|
+
// The named token held — and may never have had a value at all.
|
|
178
|
+
// `var(--ks-card-radius, var(--va-radius-md))` is the ordinary shape of a
|
|
179
|
+
// component token with a system fallback: `styleTokens` names the outer one,
|
|
180
|
+
// because that is what the author wrote, and when it is undefined the value
|
|
181
|
+
// came from the fallback. Comparing the outer name's value then compares
|
|
182
|
+
// `undefined` with `undefined`, concludes the token held, and reports an edit
|
|
183
|
+
// to the radius *scale* as `component:Card` — "Card changed internally", once
|
|
184
|
+
// per consuming component — instead of one token root with counted collateral.
|
|
185
|
+
// That is the product's headline claim failing on the most common
|
|
186
|
+
// design-system shape there is, and it survived because the corpus asserted
|
|
187
|
+
// the root *count*, which is one either way.
|
|
188
|
+
return soleMovedToken(before, after);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The one token this node resolved through whose value moved, if there is
|
|
192
|
+
* exactly one.
|
|
193
|
+
*
|
|
194
|
+
* The fallback for a chain whose outer name carries no value. Deliberately
|
|
195
|
+
* refuses to answer when two moved: "the named token held and several others
|
|
196
|
+
* changed" is genuinely ambiguous, and picking one would put a confident wrong
|
|
197
|
+
* name in front of a reviewer. Ambiguity falls through to the component root,
|
|
198
|
+
* which is coarser and true.
|
|
199
|
+
*/
|
|
200
|
+
function soleMovedToken(before, after) {
|
|
201
|
+
const moved = unionKeys(before.tokens, after.tokens).filter((name) => before.tokens?.[name] !== after.tokens?.[name]);
|
|
202
|
+
return moved.length === 1 ? moved[0] : undefined;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Properties whose initial value is `currentColor`.
|
|
206
|
+
*
|
|
207
|
+
* `color` itself is excluded, or the lookup would recurse.
|
|
208
|
+
*/
|
|
209
|
+
const FOLLOWS_CURRENT_COLOR = new Set([
|
|
210
|
+
'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
|
|
211
|
+
'outline-color', 'text-decoration-color', 'text-emphasis-color', 'column-rule-color',
|
|
212
|
+
'caret-color',
|
|
213
|
+
]);
|
|
214
|
+
function sameRect(before, after) {
|
|
215
|
+
const a = before.rect;
|
|
216
|
+
const b = after.rect;
|
|
217
|
+
if (a === undefined || b === undefined)
|
|
218
|
+
return a === b;
|
|
219
|
+
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
220
|
+
}
|
|
221
|
+
function unionKeys(a, b) {
|
|
222
|
+
return [...new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})])].sort();
|
|
223
|
+
}
|
|
224
|
+
function str(value) {
|
|
225
|
+
return value === undefined ? undefined : String(value);
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=compare-nodes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare-nodes.js","sourceRoot":"","sources":["../../../src/compare/diff/compare-nodes.ts"],"names":[],"mappings":"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","sourcesContent":["import { bandOf, type DeltaKind } from '../band.js';\nimport { impactOf } from '../impact.js';\nimport type { SemanticNode } from '../../format/snapshot.js';\nimport type { Delta } from './delta.js';\n\n/**\n * Delta production for a single node, or a single matched pair of nodes.\n *\n * Separated from `index.ts` because everything here answers a strictly local\n * question — given two nodes already known to be the same node, which fields\n * moved — and knows nothing about trees, roots, or attribution. The token\n * reasoning lives here rather than with the attributor for the same reason: the\n * evidence that decides whether a property change is collateral of a token edit\n * is the pair of nodes, and nothing further out has it.\n */\n\nexport function wholeNode(\n kind: 'node-added' | 'node-removed' | 'node-moved',\n node: SemanticNode,\n): Delta {\n return {\n kind,\n band: bandOf(kind),\n path: node.path,\n to: describe(node),\n ...(node.provenance ? { owners: node.provenance.owners } : {}),\n ...(node.provenance?.createdBy ? { createdBy: node.provenance.createdBy } : {}),\n };\n}\n\nfunction describe(node: SemanticNode): string {\n if (node.role !== undefined && node.name !== undefined) return `${node.role} \"${node.name}\"`;\n if (node.role !== undefined) return node.role;\n if (node.text !== undefined) return `${node.tag} \"${node.text}\"`;\n return node.tag;\n}\n\nexport function compareNodes(\n before: SemanticNode,\n after: SemanticNode,\n deltas: Delta[],\n hasLayout: boolean,\n): void {\n const owners = after.provenance?.owners;\n const createdBy = after.provenance?.createdBy;\n const base = (kind: DeltaKind) => ({\n kind,\n band: bandOf(kind),\n path: after.path,\n ...(owners ? { owners } : {}),\n ...(createdBy ? { createdBy } : {}),\n });\n\n if (before.role !== after.role) {\n deltas.push({ ...base('role-changed'), from: before.role, to: after.role });\n }\n if (before.name !== after.name) {\n deltas.push({ ...base('name-changed'), from: before.name, to: after.name });\n }\n if (before.description !== after.description) {\n deltas.push({\n ...base('description-changed'),\n from: before.description,\n to: after.description,\n });\n }\n if (before.text !== after.text) {\n deltas.push({ ...base('text-changed'), from: before.text, to: after.text });\n }\n\n for (const property of unionKeys(before.state, after.state)) {\n const from = before.state?.[property];\n const to = after.state?.[property];\n if (from !== to) {\n deltas.push({ ...base('state-changed'), property, from: str(from), to: str(to) });\n }\n }\n\n for (const property of unionKeys(before.attributes, after.attributes)) {\n const from = before.attributes[property];\n const to = after.attributes[property];\n if (from !== to) {\n deltas.push({ ...base('attribute-changed'), property, from, to });\n }\n }\n\n let reflowCause: Delta | null = null;\n\n for (const property of unionKeys(before.style, after.style)) {\n const from = before.style[property];\n const to = after.style[property];\n if (from === to) continue;\n\n // A token whose own value moved makes this delta collateral rather than a\n // root. Recorded here so attribution does not have to re-derive it.\n const token = changedTokenFor(before, after, property);\n const impact = impactOf(property);\n\n const delta: Delta = {\n ...base('style-changed'),\n property,\n from,\n to,\n impact,\n ...(token ? { token } : {}),\n };\n\n deltas.push(delta);\n if (impact === 'layout' && reflowCause === null) reflowCause = delta;\n }\n\n // Which token a property resolves *through* is part of the render hash, so a\n // change in it moves the hash — but under a profile that cannot resolve custom\n // properties both resolved values are the same empty string and the loop above\n // sees nothing. The result was a non-identical diff carrying zero deltas and\n // zero roots: a correct verdict with an empty docket, which tells a reviewer\n // that something changed and then refuses to say what. Found by scoring\n // `prop-size/button` under `jsdom`, which only became scorable with ADR-0008.\n //\n // Skipped when the resolved value moved too: the `style-changed` delta above\n // already names that property, and reporting both splits one cause in two.\n for (const property of unionKeys(before.styleTokens, after.styleTokens)) {\n const from = before.styleTokens?.[property];\n const to = after.styleTokens?.[property];\n if (from === to) continue;\n if (before.style[property] !== after.style[property]) continue;\n\n deltas.push({ ...base('token-changed'), property, from, to });\n }\n\n if (hasLayout && !sameRect(before, after)) {\n // A rect that moved because this node's own padding changed is the same\n // finding observed twice. Folding it under its cause — and inheriting that\n // cause's token — keeps one edit as one docket entry instead of splitting it\n // into a `token` root and an unrelated-looking `geometry` one.\n //\n // A rect that moved with *no* layout-impact change here is different and\n // stays independent: something upstream reflowed and pushed this node, which\n // is exactly the propagation worth surfacing.\n deltas.push({\n ...base('rect-changed'),\n ...(before.rect ? { rectFrom: before.rect } : {}),\n ...(after.rect ? { rectTo: after.rect } : {}),\n ...(reflowCause\n ? {\n derivedFrom: `${reflowCause.path}:${reflowCause.property ?? ''}`,\n ...(reflowCause.token ? { token: reflowCause.token } : {}),\n }\n : {}),\n });\n }\n}\n\n/**\n * The token that explains *this property's* change, if one does.\n *\n * Two conditions, and both are load-bearing. The property must actually resolve\n * through the token — a node whose `color` comes from a token and whose padding\n * comes from a literal is collateral of a token edit only in its colour. And the\n * token's own value must have moved: a node that merely mentions\n * `--color-primary` while that token held is not collateral of anything, its own\n * rule changed, and it is a root.\n *\n * Dropping either check splits one docket entry into several. Ignoring the\n * property meant a rule that overrode a token-driven value got attributed to the\n * token it had just stopped using.\n */\nfunction changedTokenFor(\n before: SemanticNode,\n after: SemanticNode,\n property: string,\n): string | undefined {\n const token = before.styleTokens?.[property];\n\n // The *same* token must drive the property on both sides. A property that\n // stopped resolving through a token did so because some rule started winning\n // instead — that rule is the root, and blaming the abandoned token would name\n // the thing that did not change.\n // Properties whose initial value is `currentColor` follow `color` without ever\n // naming it: no rule declares `outline-color`, so it resolves through no token,\n // yet a token-driven `color` change moves it. Under a profile with computed\n // style that produced a second root per Button — \"the accent token moved\" and\n // \"Button changed\" — for one edit. Recognised by the value matching `color` on\n // both sides, so a node that genuinely declares its own outline colour is\n // unaffected.\n if (token === undefined && FOLLOWS_CURRENT_COLOR.has(property)) {\n const from = before.style[property];\n const to = after.style[property];\n if (from !== undefined && to !== undefined && from === before.style['color'] && to === after.style['color']) {\n return changedTokenFor(before, after, 'color');\n }\n return undefined;\n }\n\n if (token === undefined || after.styleTokens?.[property] !== token) return undefined;\n\n // Undefined on one side counts as a change: a theme override introducing a\n // token that previously had no value is exactly the case the token band exists\n // to collapse into one root.\n if (before.tokens?.[token] !== after.tokens?.[token]) return token;\n\n // The named token held — and may never have had a value at all.\n // `var(--ks-card-radius, var(--va-radius-md))` is the ordinary shape of a\n // component token with a system fallback: `styleTokens` names the outer one,\n // because that is what the author wrote, and when it is undefined the value\n // came from the fallback. Comparing the outer name's value then compares\n // `undefined` with `undefined`, concludes the token held, and reports an edit\n // to the radius *scale* as `component:Card` — \"Card changed internally\", once\n // per consuming component — instead of one token root with counted collateral.\n // That is the product's headline claim failing on the most common\n // design-system shape there is, and it survived because the corpus asserted\n // the root *count*, which is one either way.\n return soleMovedToken(before, after);\n}\n\n/**\n * The one token this node resolved through whose value moved, if there is\n * exactly one.\n *\n * The fallback for a chain whose outer name carries no value. Deliberately\n * refuses to answer when two moved: \"the named token held and several others\n * changed\" is genuinely ambiguous, and picking one would put a confident wrong\n * name in front of a reviewer. Ambiguity falls through to the component root,\n * which is coarser and true.\n */\nfunction soleMovedToken(before: SemanticNode, after: SemanticNode): string | undefined {\n const moved = unionKeys(before.tokens, after.tokens).filter(\n (name) => before.tokens?.[name] !== after.tokens?.[name],\n );\n\n return moved.length === 1 ? moved[0] : undefined;\n}\n\n/**\n * Properties whose initial value is `currentColor`.\n *\n * `color` itself is excluded, or the lookup would recurse.\n */\nconst FOLLOWS_CURRENT_COLOR: ReadonlySet<string> = new Set([\n 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',\n 'outline-color', 'text-decoration-color', 'text-emphasis-color', 'column-rule-color',\n 'caret-color',\n]);\n\nfunction sameRect(before: SemanticNode, after: SemanticNode): boolean {\n const a = before.rect;\n const b = after.rect;\n if (a === undefined || b === undefined) return a === b;\n return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;\n}\n\nfunction unionKeys(\n a: Readonly<Record<string, unknown>> | undefined,\n b: Readonly<Record<string, unknown>> | undefined,\n): readonly string[] {\n return [...new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})])].sort();\n}\n\nfunction str(value: unknown): string | undefined {\n return value === undefined ? undefined : String(value);\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ChangedComponent, type Delta, type Root } from './delta.js';
|
|
2
|
+
/**
|
|
3
|
+
* The component roll-up: who caused this change set, and who merely renders it.
|
|
4
|
+
*
|
|
5
|
+
* Kept apart from attribution because it runs *after* it and asks a different
|
|
6
|
+
* question. Attribution decides what one sentence explains the deltas;
|
|
7
|
+
* this decides which components that sentence implicates, and in what capacity.
|
|
8
|
+
* The two share only the roots, which is exactly the argument for one being able
|
|
9
|
+
* to change without disturbing the other.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Which components a change set implicates, and in what capacity.
|
|
13
|
+
*
|
|
14
|
+
* A component is a *root* when a root's own deltas name it innermost — the change
|
|
15
|
+
* originated there. It is *collateral* when it only ever appears further out in
|
|
16
|
+
* an owner chain, or under a token root: it renders something that changed, but
|
|
17
|
+
* nothing about it changed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function componentsOf(deltas: readonly Delta[], roots: readonly Root[]): readonly ChangedComponent[];
|
|
20
|
+
//# sourceMappingURL=components.d.ts.map
|