@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,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render impact: what a changed property can actually disturb.
|
|
3
|
+
*
|
|
4
|
+
* This is a **second axis**, orthogonal to the frequency bands of §5, and adding
|
|
5
|
+
* it rather than folding it into them is deliberate. A band answers *what kind of
|
|
6
|
+
* thing changed* — structure, a value, sub-pixel noise. Impact answers *how far
|
|
7
|
+
* the change can reach*.
|
|
8
|
+
*
|
|
9
|
+
* The two genuinely differ. A spacing token and a colour token are both `token`
|
|
10
|
+
* band: a value moved, structure held. But changing spacing reflows the document,
|
|
11
|
+
* so it can move a node on the other side of the page; changing colour repaints
|
|
12
|
+
* one box and can move nothing at all. Reporting both as "a token changed" hides
|
|
13
|
+
* the only thing a reviewer wants to know first.
|
|
14
|
+
*
|
|
15
|
+
* The useful consequence is a **bound on collateral**:
|
|
16
|
+
*
|
|
17
|
+
* > A paint-only change has no geometric collateral. Ever.
|
|
18
|
+
*
|
|
19
|
+
* That is not a heuristic — it follows from how rendering works. So a subject
|
|
20
|
+
* whose every delta is paint-impact needs no layout comparison to rule out
|
|
21
|
+
* geometry regressions, which is exactly the kind of question a profile without a
|
|
22
|
+
* layout engine can otherwise never settle. It is how the cheap tier answers
|
|
23
|
+
* *"could this have moved anything?"* without measuring anything.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Properties that change the box tree.
|
|
27
|
+
*
|
|
28
|
+
* Includes the typography metrics, which is easy to get wrong: `font-size`,
|
|
29
|
+
* `line-height`, `letter-spacing`, and `text-transform` all change glyph
|
|
30
|
+
* advances, so they resize the boxes containing them and reflow everything after.
|
|
31
|
+
* A "typography token" change is a layout change, not a styling one.
|
|
32
|
+
*/
|
|
33
|
+
const LAYOUT = [
|
|
34
|
+
'display', 'position', 'top', 'right', 'bottom', 'left', 'float', 'clear',
|
|
35
|
+
'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
|
|
36
|
+
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
37
|
+
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
38
|
+
'box-sizing', 'overflow-x', 'overflow-y', 'aspect-ratio',
|
|
39
|
+
'flex-direction', 'flex-wrap', 'flex-grow', 'flex-shrink', 'flex-basis',
|
|
40
|
+
'justify-content', 'align-items', 'align-self', 'align-content', 'order',
|
|
41
|
+
'grid-template-columns', 'grid-template-rows', 'grid-template-areas',
|
|
42
|
+
'grid-auto-columns', 'grid-auto-rows', 'grid-auto-flow',
|
|
43
|
+
'grid-column-start', 'grid-column-end', 'grid-row-start', 'grid-row-end',
|
|
44
|
+
'row-gap', 'column-gap',
|
|
45
|
+
'font-family', 'font-size', 'font-weight', 'font-style', 'font-stretch',
|
|
46
|
+
'font-variant', 'line-height', 'letter-spacing', 'word-spacing',
|
|
47
|
+
'text-align', 'text-indent', 'text-transform', 'white-space', 'word-break',
|
|
48
|
+
'overflow-wrap', 'text-overflow', 'vertical-align', 'writing-mode', 'direction',
|
|
49
|
+
// Border *width* occupies space; border colour does not. The style longhand
|
|
50
|
+
// belongs here too, because `none` collapses the border to zero width whatever
|
|
51
|
+
// the declared width says.
|
|
52
|
+
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
|
|
53
|
+
'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
|
|
54
|
+
'table-layout', 'border-collapse', 'border-spacing', 'caption-side',
|
|
55
|
+
'content-visibility',
|
|
56
|
+
];
|
|
57
|
+
/** Properties that repaint a box without resizing or moving it. */
|
|
58
|
+
const PAINT = [
|
|
59
|
+
'color', 'background-color', 'background-image', 'background-position',
|
|
60
|
+
'background-size', 'background-repeat', 'background-clip', 'background-origin',
|
|
61
|
+
'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
|
|
62
|
+
'border-top-left-radius', 'border-top-right-radius',
|
|
63
|
+
'border-bottom-right-radius', 'border-bottom-left-radius',
|
|
64
|
+
'box-shadow', 'text-shadow',
|
|
65
|
+
'text-decoration-line', 'text-decoration-color', 'text-decoration-style',
|
|
66
|
+
'text-decoration-thickness',
|
|
67
|
+
'object-fit', 'object-position',
|
|
68
|
+
// `outline` is painted outside the box and never affects layout — that is the
|
|
69
|
+
// whole reason it exists as a separate property from `border`.
|
|
70
|
+
'outline-width', 'outline-style', 'outline-color', 'outline-offset',
|
|
71
|
+
// `visibility: hidden` still occupies its space, unlike `display: none`.
|
|
72
|
+
'visibility',
|
|
73
|
+
];
|
|
74
|
+
/** Properties handled by the compositor. */
|
|
75
|
+
const COMPOSITE = [
|
|
76
|
+
'transform', 'transform-origin', 'opacity', 'filter', 'backdrop-filter',
|
|
77
|
+
'mix-blend-mode', 'z-index',
|
|
78
|
+
];
|
|
79
|
+
const IMPACTS = new Map([
|
|
80
|
+
...LAYOUT.map((property) => [property, 'layout']),
|
|
81
|
+
...PAINT.map((property) => [property, 'paint']),
|
|
82
|
+
...COMPOSITE.map((property) => [property, 'composite']),
|
|
83
|
+
]);
|
|
84
|
+
/**
|
|
85
|
+
* Impact of a property change.
|
|
86
|
+
*
|
|
87
|
+
* An unrecognized property is reported as `layout`, the widest answer. Impact is
|
|
88
|
+
* used to *rule out* collateral, so an unknown treated as paint-only would let a
|
|
89
|
+
* real reflow pass unexamined — the over-reporting direction, as everywhere else.
|
|
90
|
+
*/
|
|
91
|
+
export function impactOf(property) {
|
|
92
|
+
// A custom property is only ever a carrier; what it costs depends entirely on
|
|
93
|
+
// where it was consumed, which the caller knows and this function does not.
|
|
94
|
+
if (property.startsWith('--'))
|
|
95
|
+
return 'layout';
|
|
96
|
+
return IMPACTS.get(property) ?? 'layout';
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Whether a change to this property can move anything.
|
|
100
|
+
*
|
|
101
|
+
* The question the cheap tier can answer without a layout engine, and the reason
|
|
102
|
+
* this axis exists.
|
|
103
|
+
*/
|
|
104
|
+
export function canReflow(property) {
|
|
105
|
+
return impactOf(property) === 'layout';
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Combine impacts for a group of changes.
|
|
109
|
+
*
|
|
110
|
+
* `structural` wins over everything: a node appearing or disappearing is not a
|
|
111
|
+
* property change at all, and describing it in terms of repaint versus reflow
|
|
112
|
+
* would understate it.
|
|
113
|
+
*/
|
|
114
|
+
export function aggregateImpact(impacts) {
|
|
115
|
+
if (impacts.length === 0)
|
|
116
|
+
return 'paint';
|
|
117
|
+
if (impacts.includes('structural'))
|
|
118
|
+
return 'structural';
|
|
119
|
+
const distinct = new Set(impacts);
|
|
120
|
+
if (distinct.size === 1)
|
|
121
|
+
return [...distinct][0];
|
|
122
|
+
return 'mixed';
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=impact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"impact.js","sourceRoot":"","sources":["../../src/compare/impact.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Render impact: what a changed property can actually disturb.\n *\n * This is a **second axis**, orthogonal to the frequency bands of §5, and adding\n * it rather than folding it into them is deliberate. A band answers *what kind of\n * thing changed* — structure, a value, sub-pixel noise. Impact answers *how far\n * the change can reach*.\n *\n * The two genuinely differ. A spacing token and a colour token are both `token`\n * band: a value moved, structure held. But changing spacing reflows the document,\n * so it can move a node on the other side of the page; changing colour repaints\n * one box and can move nothing at all. Reporting both as \"a token changed\" hides\n * the only thing a reviewer wants to know first.\n *\n * The useful consequence is a **bound on collateral**:\n *\n * > A paint-only change has no geometric collateral. Ever.\n *\n * That is not a heuristic — it follows from how rendering works. So a subject\n * whose every delta is paint-impact needs no layout comparison to rule out\n * geometry regressions, which is exactly the kind of question a profile without a\n * layout engine can otherwise never settle. It is how the cheap tier answers\n * *\"could this have moved anything?\"* without measuring anything.\n */\n\nexport type PropertyImpact =\n /** Reflows: box sizes and positions may change, here and elsewhere. */\n | 'layout'\n /** Repaints in place. The box tree is untouched, so nothing moves. */\n | 'paint'\n /** Compositing only: transform, opacity, stacking. No reflow, no repaint. */\n | 'composite';\n\n/**\n * Properties that change the box tree.\n *\n * Includes the typography metrics, which is easy to get wrong: `font-size`,\n * `line-height`, `letter-spacing`, and `text-transform` all change glyph\n * advances, so they resize the boxes containing them and reflow everything after.\n * A \"typography token\" change is a layout change, not a styling one.\n */\nconst LAYOUT: readonly string[] = [\n 'display', 'position', 'top', 'right', 'bottom', 'left', 'float', 'clear',\n 'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',\n 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',\n 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',\n 'box-sizing', 'overflow-x', 'overflow-y', 'aspect-ratio',\n\n 'flex-direction', 'flex-wrap', 'flex-grow', 'flex-shrink', 'flex-basis',\n 'justify-content', 'align-items', 'align-self', 'align-content', 'order',\n 'grid-template-columns', 'grid-template-rows', 'grid-template-areas',\n 'grid-auto-columns', 'grid-auto-rows', 'grid-auto-flow',\n 'grid-column-start', 'grid-column-end', 'grid-row-start', 'grid-row-end',\n 'row-gap', 'column-gap',\n\n 'font-family', 'font-size', 'font-weight', 'font-style', 'font-stretch',\n 'font-variant', 'line-height', 'letter-spacing', 'word-spacing',\n 'text-align', 'text-indent', 'text-transform', 'white-space', 'word-break',\n 'overflow-wrap', 'text-overflow', 'vertical-align', 'writing-mode', 'direction',\n\n // Border *width* occupies space; border colour does not. The style longhand\n // belongs here too, because `none` collapses the border to zero width whatever\n // the declared width says.\n 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',\n 'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',\n\n 'table-layout', 'border-collapse', 'border-spacing', 'caption-side',\n 'content-visibility',\n];\n\n/** Properties that repaint a box without resizing or moving it. */\nconst PAINT: readonly string[] = [\n 'color', 'background-color', 'background-image', 'background-position',\n 'background-size', 'background-repeat', 'background-clip', 'background-origin',\n 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',\n 'border-top-left-radius', 'border-top-right-radius',\n 'border-bottom-right-radius', 'border-bottom-left-radius',\n 'box-shadow', 'text-shadow',\n 'text-decoration-line', 'text-decoration-color', 'text-decoration-style',\n 'text-decoration-thickness',\n 'object-fit', 'object-position',\n\n // `outline` is painted outside the box and never affects layout — that is the\n // whole reason it exists as a separate property from `border`.\n 'outline-width', 'outline-style', 'outline-color', 'outline-offset',\n\n // `visibility: hidden` still occupies its space, unlike `display: none`.\n 'visibility',\n];\n\n/** Properties handled by the compositor. */\nconst COMPOSITE: readonly string[] = [\n 'transform', 'transform-origin', 'opacity', 'filter', 'backdrop-filter',\n 'mix-blend-mode', 'z-index',\n];\n\nconst IMPACTS = new Map<string, PropertyImpact>([\n ...LAYOUT.map((property) => [property, 'layout'] as const),\n ...PAINT.map((property) => [property, 'paint'] as const),\n ...COMPOSITE.map((property) => [property, 'composite'] as const),\n]);\n\n/**\n * Impact of a property change.\n *\n * An unrecognized property is reported as `layout`, the widest answer. Impact is\n * used to *rule out* collateral, so an unknown treated as paint-only would let a\n * real reflow pass unexamined — the over-reporting direction, as everywhere else.\n */\nexport function impactOf(property: string): PropertyImpact {\n // A custom property is only ever a carrier; what it costs depends entirely on\n // where it was consumed, which the caller knows and this function does not.\n if (property.startsWith('--')) return 'layout';\n return IMPACTS.get(property) ?? 'layout';\n}\n\n/**\n * Whether a change to this property can move anything.\n *\n * The question the cheap tier can answer without a layout engine, and the reason\n * this axis exists.\n */\nexport function canReflow(property: string): boolean {\n return impactOf(property) === 'layout';\n}\n\nexport type AggregateImpact = PropertyImpact | 'mixed' | 'structural';\n\n/**\n * Combine impacts for a group of changes.\n *\n * `structural` wins over everything: a node appearing or disappearing is not a\n * property change at all, and describing it in terms of repaint versus reflow\n * would understate it.\n */\nexport function aggregateImpact(\n impacts: readonly (PropertyImpact | 'structural')[],\n): AggregateImpact {\n if (impacts.length === 0) return 'paint';\n if (impacts.includes('structural')) return 'structural';\n\n const distinct = new Set(impacts);\n if (distinct.size === 1) return [...distinct][0] as PropertyImpact;\n return 'mixed';\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@variance-authority/core/compare` — two snapshots become a list of deltas.
|
|
3
|
+
*
|
|
4
|
+
* Tree matching, the delta vocabulary, the bands a delta falls into, and what a
|
|
5
|
+
* changed property is capable of doing to a layout. No verdict: this group says
|
|
6
|
+
* *what moved*, and refuses to say whether anyone should mind.
|
|
7
|
+
*
|
|
8
|
+
* The split from `judge` is the load-bearing one. A comparison that also decided
|
|
9
|
+
* severity could not be reused by a caller with a different policy, and every
|
|
10
|
+
* team has a different policy.
|
|
11
|
+
*/
|
|
12
|
+
export { diffSnapshots, matchTrees } from './diff/index.js';
|
|
13
|
+
export type { SemanticDiff, Delta, Root, RootKind, ChangedComponent, Matching, } from './diff/index.js';
|
|
14
|
+
export { impactOf, canReflow, aggregateImpact } from './impact.js';
|
|
15
|
+
export type { PropertyImpact, AggregateImpact } from './impact.js';
|
|
16
|
+
export type { Band, DeltaKind } from './band.js';
|
|
17
|
+
export { BANDS, bandOf, loudestBand } from './band.js';
|
|
18
|
+
export { observableBands, decidesBand } from './observability.js';
|
|
19
|
+
export type { Observability } from './observability.js';
|
|
20
|
+
export { deriveVariation } from './derive.js';
|
|
21
|
+
export type { Variation } from './derive.js';
|
|
22
|
+
export { partingOf } from './parting.js';
|
|
23
|
+
export { boundarySnapshot } from './instance.js';
|
|
24
|
+
export { sliceOf, sameTree } from './slice.js';
|
|
25
|
+
export type { PartingSlice } from './slice.js';
|
|
26
|
+
export type { Parting, PartedBoundary, PartingRung, MovedInput, } from './parting.js';
|
|
27
|
+
export { explainParting } from './explain.js';
|
|
28
|
+
export { compareValues, fingerprintOfValueDelta } from './value.js';
|
|
29
|
+
export type { ValueChange, ValueDelta } from './value.js';
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@variance-authority/core/compare` — two snapshots become a list of deltas.
|
|
3
|
+
*
|
|
4
|
+
* Tree matching, the delta vocabulary, the bands a delta falls into, and what a
|
|
5
|
+
* changed property is capable of doing to a layout. No verdict: this group says
|
|
6
|
+
* *what moved*, and refuses to say whether anyone should mind.
|
|
7
|
+
*
|
|
8
|
+
* The split from `judge` is the load-bearing one. A comparison that also decided
|
|
9
|
+
* severity could not be reused by a caller with a different policy, and every
|
|
10
|
+
* team has a different policy.
|
|
11
|
+
*/
|
|
12
|
+
export { diffSnapshots, matchTrees } from './diff/index.js';
|
|
13
|
+
export { impactOf, canReflow, aggregateImpact } from './impact.js';
|
|
14
|
+
export { BANDS, bandOf, loudestBand } from './band.js';
|
|
15
|
+
export { observableBands, decidesBand } from './observability.js';
|
|
16
|
+
export { deriveVariation } from './derive.js';
|
|
17
|
+
export { partingOf } from './parting.js';
|
|
18
|
+
export { boundarySnapshot } from './instance.js';
|
|
19
|
+
export { sliceOf, sameTree } from './slice.js';
|
|
20
|
+
export { explainParting } from './explain.js';
|
|
21
|
+
export { compareValues, fingerprintOfValueDelta } from './value.js';
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/compare/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAU5D,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAInE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEvD,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGlE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAS/C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC","sourcesContent":["/**\n * `@variance-authority/core/compare` — two snapshots become a list of deltas.\n *\n * Tree matching, the delta vocabulary, the bands a delta falls into, and what a\n * changed property is capable of doing to a layout. No verdict: this group says\n * *what moved*, and refuses to say whether anyone should mind.\n *\n * The split from `judge` is the load-bearing one. A comparison that also decided\n * severity could not be reused by a caller with a different policy, and every\n * team has a different policy.\n */\n\nexport { diffSnapshots, matchTrees } from './diff/index.js';\nexport type {\n SemanticDiff,\n Delta,\n Root,\n RootKind,\n ChangedComponent,\n Matching,\n} from './diff/index.js';\n\nexport { impactOf, canReflow, aggregateImpact } from './impact.js';\nexport type { PropertyImpact, AggregateImpact } from './impact.js';\n\nexport type { Band, DeltaKind } from './band.js';\nexport { BANDS, bandOf, loudestBand } from './band.js';\n\nexport { observableBands, decidesBand } from './observability.js';\nexport type { Observability } from './observability.js';\n\nexport { deriveVariation } from './derive.js';\nexport type { Variation } from './derive.js';\n\nexport { partingOf } from './parting.js';\nexport { boundarySnapshot } from './instance.js';\nexport { sliceOf, sameTree } from './slice.js';\nexport type { PartingSlice } from './slice.js';\nexport type {\n Parting,\n PartedBoundary,\n PartingRung,\n MovedInput,\n} from './parting.js';\n\nexport { explainParting } from './explain.js';\n\nexport { compareValues, fingerprintOfValueDelta } from './value.js';\nexport type { ValueChange, ValueDelta } from './value.js';\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { NodePath, SemanticSnapshot } from '../format/snapshot.js';
|
|
2
|
+
/**
|
|
3
|
+
* One component instance, read as though it were the whole subject.
|
|
4
|
+
*
|
|
5
|
+
* `partingOf` compares two readings of one tree. A divergence is not that: it is
|
|
6
|
+
* one component reached at two *sites*, in two different subjects, and handing
|
|
7
|
+
* the two subjects straight to a parting produces a confident wrong answer.
|
|
8
|
+
* Measured on the divergence example, `Price` — the component the finding is
|
|
9
|
+
* about, present on both sides — came back `unpaired`, while `Receipt` and
|
|
10
|
+
* `PromoCard`, which are genuinely different components, paired positionally at
|
|
11
|
+
* the root and were reported nondeterministic for holding no state.
|
|
12
|
+
*
|
|
13
|
+
* The cause is that a boundary's identity is its owner chain, and the chain
|
|
14
|
+
* carries the container: the same `<Price>` is `Price>Receipt` at one site and
|
|
15
|
+
* `Price>PromoCard` at the other, so neither `pairBoundaries` nor `sameTree`
|
|
16
|
+
* can see them as one component. That is correct for the question those
|
|
17
|
+
* functions were built for — two arms of an experiment share a root, and a
|
|
18
|
+
* container that differs there really is a difference — and wrong for this one.
|
|
19
|
+
*
|
|
20
|
+
* So the instance is lifted out before it is compared. Everything outside it
|
|
21
|
+
* goes, including the part of every owner chain that named it: what is left is
|
|
22
|
+
* the subtree that component rendered, addressed from its own root, which is
|
|
23
|
+
* the thing two sites actually have in common.
|
|
24
|
+
*
|
|
25
|
+
* Returns `undefined` when the path names no node, rather than an empty
|
|
26
|
+
* snapshot, so a caller cannot mistake "that site is not in this tree" for
|
|
27
|
+
* "that site rendered nothing".
|
|
28
|
+
*/
|
|
29
|
+
export declare function boundarySnapshot(snapshot: SemanticSnapshot, path: NodePath): SemanticSnapshot | undefined;
|
|
30
|
+
//# sourceMappingURL=instance.d.ts.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { digestCombine, digestValue } from '../format/hash.js';
|
|
2
|
+
import { structureOf, styleOf } from '../rules/normalize/project.js';
|
|
3
|
+
/**
|
|
4
|
+
* One component instance, read as though it were the whole subject.
|
|
5
|
+
*
|
|
6
|
+
* `partingOf` compares two readings of one tree. A divergence is not that: it is
|
|
7
|
+
* one component reached at two *sites*, in two different subjects, and handing
|
|
8
|
+
* the two subjects straight to a parting produces a confident wrong answer.
|
|
9
|
+
* Measured on the divergence example, `Price` — the component the finding is
|
|
10
|
+
* about, present on both sides — came back `unpaired`, while `Receipt` and
|
|
11
|
+
* `PromoCard`, which are genuinely different components, paired positionally at
|
|
12
|
+
* the root and were reported nondeterministic for holding no state.
|
|
13
|
+
*
|
|
14
|
+
* The cause is that a boundary's identity is its owner chain, and the chain
|
|
15
|
+
* carries the container: the same `<Price>` is `Price>Receipt` at one site and
|
|
16
|
+
* `Price>PromoCard` at the other, so neither `pairBoundaries` nor `sameTree`
|
|
17
|
+
* can see them as one component. That is correct for the question those
|
|
18
|
+
* functions were built for — two arms of an experiment share a root, and a
|
|
19
|
+
* container that differs there really is a difference — and wrong for this one.
|
|
20
|
+
*
|
|
21
|
+
* So the instance is lifted out before it is compared. Everything outside it
|
|
22
|
+
* goes, including the part of every owner chain that named it: what is left is
|
|
23
|
+
* the subtree that component rendered, addressed from its own root, which is
|
|
24
|
+
* the thing two sites actually have in common.
|
|
25
|
+
*
|
|
26
|
+
* Returns `undefined` when the path names no node, rather than an empty
|
|
27
|
+
* snapshot, so a caller cannot mistake "that site is not in this tree" for
|
|
28
|
+
* "that site rendered nothing".
|
|
29
|
+
*/
|
|
30
|
+
export function boundarySnapshot(snapshot, path) {
|
|
31
|
+
const node = nodeAt(snapshot.root, path);
|
|
32
|
+
if (node === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
/**
|
|
35
|
+
* How many owners to drop, decided once at the boundary and applied to every
|
|
36
|
+
* node under it.
|
|
37
|
+
*
|
|
38
|
+
* Owners run innermost-first, so the containers outside this instance are the
|
|
39
|
+
* *tail* of the list — and they are the same tail at every depth, because
|
|
40
|
+
* every node in the subtree is inside the same containers. `Price>PromoCard`
|
|
41
|
+
* loses one and becomes `Price`; a `Label` nested inside it reads
|
|
42
|
+
* `Label>Price>PromoCard` and loses exactly the same one.
|
|
43
|
+
*/
|
|
44
|
+
const outer = Math.max(0, (node.provenance?.owners.length ?? 1) - 1);
|
|
45
|
+
const root = lift(node, path, outer);
|
|
46
|
+
const structureHash = digestValue(structureOf(root));
|
|
47
|
+
const styleHash = digestValue(styleOf(root, snapshot.profile.layout));
|
|
48
|
+
const ignoreSites = rebasedSites(snapshot.ignoreSites, path);
|
|
49
|
+
return {
|
|
50
|
+
formatVersion: snapshot.formatVersion,
|
|
51
|
+
subject: snapshot.subject,
|
|
52
|
+
profile: snapshot.profile,
|
|
53
|
+
environment: snapshot.environment,
|
|
54
|
+
/**
|
|
55
|
+
* Recomputed, and it is not bookkeeping.
|
|
56
|
+
*
|
|
57
|
+
* `compareTrees` returns `identical` the moment two `renderHash`es match, so
|
|
58
|
+
* a lifted snapshot carrying the digest of the subject it came out of would
|
|
59
|
+
* make every comparison between two instances answer a question about their
|
|
60
|
+
* containers. Two identical renderings of one component in two different
|
|
61
|
+
* pages would compare as different, which is precisely the echo this half of
|
|
62
|
+
* the report exists to recognise.
|
|
63
|
+
*/
|
|
64
|
+
renderHash: digestCombine('render', [
|
|
65
|
+
snapshot.environment.semanticDigest,
|
|
66
|
+
structureHash,
|
|
67
|
+
styleHash,
|
|
68
|
+
]),
|
|
69
|
+
structureHash,
|
|
70
|
+
styleHash,
|
|
71
|
+
root,
|
|
72
|
+
styleProvenance: rebasedProvenance(snapshot.styleProvenance, path),
|
|
73
|
+
...(ignoreSites.length > 0 ? { ignoreSites } : {}),
|
|
74
|
+
diagnostics: snapshot.diagnostics,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The subtree, re-addressed and with the containers stripped from its chains.
|
|
79
|
+
*
|
|
80
|
+
* Paths are rewritten rather than kept, because a delta carries a path and a
|
|
81
|
+
* reader of one instance has no way to resolve `0/3/1/0` against a tree whose
|
|
82
|
+
* root is `0/3/1`. The rewrite is a prefix swap, so sibling order and depth
|
|
83
|
+
* survive it exactly.
|
|
84
|
+
*/
|
|
85
|
+
function lift(node, prefix, outer) {
|
|
86
|
+
const owners = node.provenance?.owners;
|
|
87
|
+
return {
|
|
88
|
+
...node,
|
|
89
|
+
path: rebase(node.path, prefix),
|
|
90
|
+
...(node.provenance !== undefined && owners !== undefined
|
|
91
|
+
? {
|
|
92
|
+
provenance: {
|
|
93
|
+
...node.provenance,
|
|
94
|
+
owners: owners.slice(0, Math.max(1, owners.length - outer)),
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
: {}),
|
|
98
|
+
children: node.children.map((child) => lift(child, prefix, outer)),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A path in the lifted tree's own space.
|
|
103
|
+
*
|
|
104
|
+
* The boundary itself becomes `ROOT`; everything under it keeps its tail. A path
|
|
105
|
+
* that is not under the prefix never reaches here — both callers filter first —
|
|
106
|
+
* and returning it unchanged would be a silent mis-address.
|
|
107
|
+
*/
|
|
108
|
+
function rebase(path, prefix) {
|
|
109
|
+
return path === prefix ? ROOT : `${ROOT}${path.slice(prefix.length)}`;
|
|
110
|
+
}
|
|
111
|
+
function under(path, prefix) {
|
|
112
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
113
|
+
}
|
|
114
|
+
function rebasedProvenance(entries, prefix) {
|
|
115
|
+
return entries
|
|
116
|
+
.filter((entry) => under(entry.path, prefix))
|
|
117
|
+
.map((entry) => ({ ...entry, path: rebase(entry.path, prefix) }));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Ignores kept rather than dropped.
|
|
121
|
+
*
|
|
122
|
+
* An ignore that fell inside this instance still applies to it: dropping it here
|
|
123
|
+
* would let a masked clock back into a comparison by the side door, which is the
|
|
124
|
+
* one direction this system is not allowed to be wrong in.
|
|
125
|
+
*/
|
|
126
|
+
function rebasedSites(sites, prefix) {
|
|
127
|
+
return (sites ?? [])
|
|
128
|
+
.filter((site) => under(site.path, prefix))
|
|
129
|
+
.map((site) => ({ ...site, path: rebase(site.path, prefix) }));
|
|
130
|
+
}
|
|
131
|
+
function nodeAt(root, path) {
|
|
132
|
+
if (root.path === path)
|
|
133
|
+
return root;
|
|
134
|
+
if (!under(path, root.path))
|
|
135
|
+
return undefined;
|
|
136
|
+
for (const child of root.children) {
|
|
137
|
+
const found = nodeAt(child, path);
|
|
138
|
+
if (found !== undefined)
|
|
139
|
+
return found;
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
/** The path a normalized tree's root carries, and therefore a lifted one's. */
|
|
144
|
+
const ROOT = '0';
|
|
145
|
+
//# sourceMappingURL=instance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance.js","sourceRoot":"","sources":["../../src/compare/instance.ts"],"names":[],"mappings":"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","sourcesContent":["import { digestCombine, digestValue } from '../format/hash.js';\nimport type {\n IgnoreSite,\n NodePath,\n SemanticNode,\n SemanticSnapshot,\n StyleProvenanceEntry,\n} from '../format/snapshot.js';\nimport { structureOf, styleOf } from '../rules/normalize/project.js';\n\n/**\n * One component instance, read as though it were the whole subject.\n *\n * `partingOf` compares two readings of one tree. A divergence is not that: it is\n * one component reached at two *sites*, in two different subjects, and handing\n * the two subjects straight to a parting produces a confident wrong answer.\n * Measured on the divergence example, `Price` — the component the finding is\n * about, present on both sides — came back `unpaired`, while `Receipt` and\n * `PromoCard`, which are genuinely different components, paired positionally at\n * the root and were reported nondeterministic for holding no state.\n *\n * The cause is that a boundary's identity is its owner chain, and the chain\n * carries the container: the same `<Price>` is `Price>Receipt` at one site and\n * `Price>PromoCard` at the other, so neither `pairBoundaries` nor `sameTree`\n * can see them as one component. That is correct for the question those\n * functions were built for — two arms of an experiment share a root, and a\n * container that differs there really is a difference — and wrong for this one.\n *\n * So the instance is lifted out before it is compared. Everything outside it\n * goes, including the part of every owner chain that named it: what is left is\n * the subtree that component rendered, addressed from its own root, which is\n * the thing two sites actually have in common.\n *\n * Returns `undefined` when the path names no node, rather than an empty\n * snapshot, so a caller cannot mistake \"that site is not in this tree\" for\n * \"that site rendered nothing\".\n */\nexport function boundarySnapshot(\n snapshot: SemanticSnapshot,\n path: NodePath,\n): SemanticSnapshot | undefined {\n const node = nodeAt(snapshot.root, path);\n if (node === undefined) return undefined;\n\n /**\n * How many owners to drop, decided once at the boundary and applied to every\n * node under it.\n *\n * Owners run innermost-first, so the containers outside this instance are the\n * *tail* of the list — and they are the same tail at every depth, because\n * every node in the subtree is inside the same containers. `Price>PromoCard`\n * loses one and becomes `Price`; a `Label` nested inside it reads\n * `Label>Price>PromoCard` and loses exactly the same one.\n */\n const outer = Math.max(0, (node.provenance?.owners.length ?? 1) - 1);\n\n const root = lift(node, path, outer);\n const structureHash = digestValue(structureOf(root));\n const styleHash = digestValue(styleOf(root, snapshot.profile.layout));\n const ignoreSites = rebasedSites(snapshot.ignoreSites, path);\n\n return {\n formatVersion: snapshot.formatVersion,\n subject: snapshot.subject,\n profile: snapshot.profile,\n environment: snapshot.environment,\n\n /**\n * Recomputed, and it is not bookkeeping.\n *\n * `compareTrees` returns `identical` the moment two `renderHash`es match, so\n * a lifted snapshot carrying the digest of the subject it came out of would\n * make every comparison between two instances answer a question about their\n * containers. Two identical renderings of one component in two different\n * pages would compare as different, which is precisely the echo this half of\n * the report exists to recognise.\n */\n renderHash: digestCombine('render', [\n snapshot.environment.semanticDigest,\n structureHash,\n styleHash,\n ]),\n structureHash,\n styleHash,\n root,\n styleProvenance: rebasedProvenance(snapshot.styleProvenance, path),\n ...(ignoreSites.length > 0 ? { ignoreSites } : {}),\n diagnostics: snapshot.diagnostics,\n };\n}\n\n/**\n * The subtree, re-addressed and with the containers stripped from its chains.\n *\n * Paths are rewritten rather than kept, because a delta carries a path and a\n * reader of one instance has no way to resolve `0/3/1/0` against a tree whose\n * root is `0/3/1`. The rewrite is a prefix swap, so sibling order and depth\n * survive it exactly.\n */\nfunction lift(node: SemanticNode, prefix: NodePath, outer: number): SemanticNode {\n const owners = node.provenance?.owners;\n\n return {\n ...node,\n path: rebase(node.path, prefix),\n ...(node.provenance !== undefined && owners !== undefined\n ? {\n provenance: {\n ...node.provenance,\n owners: owners.slice(0, Math.max(1, owners.length - outer)),\n },\n }\n : {}),\n children: node.children.map((child) => lift(child, prefix, outer)),\n };\n}\n\n/**\n * A path in the lifted tree's own space.\n *\n * The boundary itself becomes `ROOT`; everything under it keeps its tail. A path\n * that is not under the prefix never reaches here — both callers filter first —\n * and returning it unchanged would be a silent mis-address.\n */\nfunction rebase(path: NodePath, prefix: NodePath): NodePath {\n return path === prefix ? ROOT : `${ROOT}${path.slice(prefix.length)}`;\n}\n\nfunction under(path: NodePath, prefix: NodePath): boolean {\n return path === prefix || path.startsWith(`${prefix}/`);\n}\n\nfunction rebasedProvenance(\n entries: readonly StyleProvenanceEntry[],\n prefix: NodePath,\n): readonly StyleProvenanceEntry[] {\n return entries\n .filter((entry) => under(entry.path, prefix))\n .map((entry) => ({ ...entry, path: rebase(entry.path, prefix) }));\n}\n\n/**\n * Ignores kept rather than dropped.\n *\n * An ignore that fell inside this instance still applies to it: dropping it here\n * would let a masked clock back into a comparison by the side door, which is the\n * one direction this system is not allowed to be wrong in.\n */\nfunction rebasedSites(\n sites: readonly IgnoreSite[] | undefined,\n prefix: NodePath,\n): readonly IgnoreSite[] {\n return (sites ?? [])\n .filter((site) => under(site.path, prefix))\n .map((site) => ({ ...site, path: rebase(site.path, prefix) }));\n}\n\nfunction nodeAt(root: SemanticNode, path: NodePath): SemanticNode | undefined {\n if (root.path === path) return root;\n if (!under(path, root.path)) return undefined;\n for (const child of root.children) {\n const found = nodeAt(child, path);\n if (found !== undefined) return found;\n }\n return undefined;\n}\n\n/** The path a normalized tree's root carries, and therefore a lifted one's. */\nconst ROOT = '0';\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a profile can decide, band by band.
|
|
3
|
+
*
|
|
4
|
+
* This lives in `compare` rather than beside `ObservationProfile` in `format`
|
|
5
|
+
* because it is a statement about *bands*, and a band classifies a delta. Core's
|
|
6
|
+
* entrypoints run format → rules → compare → attribute → judge → plan, and a
|
|
7
|
+
* `format` module importing `Band` would point that arrow backwards. The profile
|
|
8
|
+
* describes a collector's capability; mapping that capability onto what can be
|
|
9
|
+
* settled is the comparison layer's question.
|
|
10
|
+
*/
|
|
11
|
+
import type { ObservationProfile } from '../format/profile.js';
|
|
12
|
+
import type { Band } from './band.js';
|
|
13
|
+
/** How completely a profile can decide one band. */
|
|
14
|
+
export type Observability = 'full' | 'structural-only' | 'declared-only' | 'none';
|
|
15
|
+
/**
|
|
16
|
+
* Bands a profile is able to decide. A band this returns `none` for can never
|
|
17
|
+
* resolve to `unchanged` under that profile — it resolves to `unobserved`.
|
|
18
|
+
*
|
|
19
|
+
* JSDOM sees structural geometry (nodes appearing, disappearing, reordering)
|
|
20
|
+
* because that is tree shape, but not metric geometry (moves, resizes) because
|
|
21
|
+
* that requires an engine. `geometry` is therefore only partially observable,
|
|
22
|
+
* and partial observation of a blocking band is reported, never assumed away.
|
|
23
|
+
*
|
|
24
|
+
* The two bands that need no engine are worth stating rather than leaving to be
|
|
25
|
+
* inferred: **`a11y` and `content` are fully decidable in the unit-test
|
|
26
|
+
* process.** A role, an accessible name, an ARIA state and a text node are facts
|
|
27
|
+
* about a document, so a dropped `aria-label` and a changed string are settled at
|
|
28
|
+
* the cheapest tier this project has — no browser, no screenshot, no baseline
|
|
29
|
+
* image. That is the concrete form of `docs/comparison.md` §3.3: the category no
|
|
30
|
+
* raster comparison can reach is also the category that costs least to reach.
|
|
31
|
+
*
|
|
32
|
+
* The return type is `Record<Band, …>` on purpose. Declaring a new band and
|
|
33
|
+
* forgetting to say whether a profile can observe it is a compile error, not a
|
|
34
|
+
* silent `undefined` that a truthiness check reads as "no".
|
|
35
|
+
*/
|
|
36
|
+
export declare function observableBands(profile: ObservationProfile): Record<Band, Observability>;
|
|
37
|
+
/**
|
|
38
|
+
* Whether a level is enough to settle its band, or whether the band must be
|
|
39
|
+
* reported `unobserved`.
|
|
40
|
+
*
|
|
41
|
+
* The asymmetry between `geometry` and everything else is ADR-0002 and is
|
|
42
|
+
* deliberate. `structural-only` geometry is *not* enough: a profile that sees
|
|
43
|
+
* tree shape but not metrics would report a box that moved as `unchanged`, which
|
|
44
|
+
* is the one failure this product must never produce. `declared-only` style *is*
|
|
45
|
+
* enough — an author's declaration is a real answer about the token band, just a
|
|
46
|
+
* narrower one than a resolved cascade.
|
|
47
|
+
*/
|
|
48
|
+
export declare function decidesBand(band: Band, level: Observability): boolean;
|
|
49
|
+
//# sourceMappingURL=observability.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a profile can decide, band by band.
|
|
3
|
+
*
|
|
4
|
+
* This lives in `compare` rather than beside `ObservationProfile` in `format`
|
|
5
|
+
* because it is a statement about *bands*, and a band classifies a delta. Core's
|
|
6
|
+
* entrypoints run format → rules → compare → attribute → judge → plan, and a
|
|
7
|
+
* `format` module importing `Band` would point that arrow backwards. The profile
|
|
8
|
+
* describes a collector's capability; mapping that capability onto what can be
|
|
9
|
+
* settled is the comparison layer's question.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Bands a profile is able to decide. A band this returns `none` for can never
|
|
13
|
+
* resolve to `unchanged` under that profile — it resolves to `unobserved`.
|
|
14
|
+
*
|
|
15
|
+
* JSDOM sees structural geometry (nodes appearing, disappearing, reordering)
|
|
16
|
+
* because that is tree shape, but not metric geometry (moves, resizes) because
|
|
17
|
+
* that requires an engine. `geometry` is therefore only partially observable,
|
|
18
|
+
* and partial observation of a blocking band is reported, never assumed away.
|
|
19
|
+
*
|
|
20
|
+
* The two bands that need no engine are worth stating rather than leaving to be
|
|
21
|
+
* inferred: **`a11y` and `content` are fully decidable in the unit-test
|
|
22
|
+
* process.** A role, an accessible name, an ARIA state and a text node are facts
|
|
23
|
+
* about a document, so a dropped `aria-label` and a changed string are settled at
|
|
24
|
+
* the cheapest tier this project has — no browser, no screenshot, no baseline
|
|
25
|
+
* image. That is the concrete form of `docs/comparison.md` §3.3: the category no
|
|
26
|
+
* raster comparison can reach is also the category that costs least to reach.
|
|
27
|
+
*
|
|
28
|
+
* The return type is `Record<Band, …>` on purpose. Declaring a new band and
|
|
29
|
+
* forgetting to say whether a profile can observe it is a compile error, not a
|
|
30
|
+
* silent `undefined` that a truthiness check reads as "no".
|
|
31
|
+
*/
|
|
32
|
+
export function observableBands(profile) {
|
|
33
|
+
return {
|
|
34
|
+
a11y: profile.ariaTree ? 'full' : 'none',
|
|
35
|
+
geometry: profile.layout ? 'full' : profile.ariaTree ? 'structural-only' : 'none',
|
|
36
|
+
token: profile.computedStyle ? 'full' : profile.declaredStyle ? 'declared-only' : 'none',
|
|
37
|
+
// Text is the one band no collector can fail at: having a document is the
|
|
38
|
+
// precondition for producing a snapshot at all.
|
|
39
|
+
content: 'full',
|
|
40
|
+
texture: profile.raster ? 'full' : 'none',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether a level is enough to settle its band, or whether the band must be
|
|
45
|
+
* reported `unobserved`.
|
|
46
|
+
*
|
|
47
|
+
* The asymmetry between `geometry` and everything else is ADR-0002 and is
|
|
48
|
+
* deliberate. `structural-only` geometry is *not* enough: a profile that sees
|
|
49
|
+
* tree shape but not metrics would report a box that moved as `unchanged`, which
|
|
50
|
+
* is the one failure this product must never produce. `declared-only` style *is*
|
|
51
|
+
* enough — an author's declaration is a real answer about the token band, just a
|
|
52
|
+
* narrower one than a resolved cascade.
|
|
53
|
+
*/
|
|
54
|
+
export function decidesBand(band, level) {
|
|
55
|
+
return band === 'geometry' ? level === 'full' : level !== 'none';
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=observability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.js","sourceRoot":"","sources":["../../src/compare/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,eAAe,CAAC,OAA2B;IACzD,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;QACxC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM;QACjF,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,MAAM;QACxF,0EAA0E;QAC1E,gDAAgD;QAChD,OAAO,EAAE,MAAM;QACf,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;KAC1C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,IAAU,EAAE,KAAoB;IAC1D,OAAO,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC;AACnE,CAAC","sourcesContent":["/**\n * What a profile can decide, band by band.\n *\n * This lives in `compare` rather than beside `ObservationProfile` in `format`\n * because it is a statement about *bands*, and a band classifies a delta. Core's\n * entrypoints run format → rules → compare → attribute → judge → plan, and a\n * `format` module importing `Band` would point that arrow backwards. The profile\n * describes a collector's capability; mapping that capability onto what can be\n * settled is the comparison layer's question.\n */\n\nimport type { ObservationProfile } from '../format/profile.js';\nimport type { Band } from './band.js';\n\n/** How completely a profile can decide one band. */\nexport type Observability = 'full' | 'structural-only' | 'declared-only' | 'none';\n\n/**\n * Bands a profile is able to decide. A band this returns `none` for can never\n * resolve to `unchanged` under that profile — it resolves to `unobserved`.\n *\n * JSDOM sees structural geometry (nodes appearing, disappearing, reordering)\n * because that is tree shape, but not metric geometry (moves, resizes) because\n * that requires an engine. `geometry` is therefore only partially observable,\n * and partial observation of a blocking band is reported, never assumed away.\n *\n * The two bands that need no engine are worth stating rather than leaving to be\n * inferred: **`a11y` and `content` are fully decidable in the unit-test\n * process.** A role, an accessible name, an ARIA state and a text node are facts\n * about a document, so a dropped `aria-label` and a changed string are settled at\n * the cheapest tier this project has — no browser, no screenshot, no baseline\n * image. That is the concrete form of `docs/comparison.md` §3.3: the category no\n * raster comparison can reach is also the category that costs least to reach.\n *\n * The return type is `Record<Band, …>` on purpose. Declaring a new band and\n * forgetting to say whether a profile can observe it is a compile error, not a\n * silent `undefined` that a truthiness check reads as \"no\".\n */\nexport function observableBands(profile: ObservationProfile): Record<Band, Observability> {\n return {\n a11y: profile.ariaTree ? 'full' : 'none',\n geometry: profile.layout ? 'full' : profile.ariaTree ? 'structural-only' : 'none',\n token: profile.computedStyle ? 'full' : profile.declaredStyle ? 'declared-only' : 'none',\n // Text is the one band no collector can fail at: having a document is the\n // precondition for producing a snapshot at all.\n content: 'full',\n texture: profile.raster ? 'full' : 'none',\n };\n}\n\n/**\n * Whether a level is enough to settle its band, or whether the band must be\n * reported `unobserved`.\n *\n * The asymmetry between `geometry` and everything else is ADR-0002 and is\n * deliberate. `structural-only` geometry is *not* enough: a profile that sees\n * tree shape but not metrics would report a box that moved as `unchanged`, which\n * is the one failure this product must never produce. `declared-only` style *is*\n * enough — an author's declaration is a real answer about the token band, just a\n * narrower one than a resolved cascade.\n */\nexport function decidesBand(band: Band, level: Observability): boolean {\n return band === 'geometry' ? level === 'full' : level !== 'none';\n}\n"]}
|