@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.
Files changed (260) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +169 -0
  4. package/dist/artifact.d.ts +47 -0
  5. package/dist/artifact.js +2 -0
  6. package/dist/artifact.js.map +1 -0
  7. package/dist/attribute/because.d.ts +43 -0
  8. package/dist/attribute/because.js +64 -0
  9. package/dist/attribute/because.js.map +1 -0
  10. package/dist/attribute/boundary.d.ts +191 -0
  11. package/dist/attribute/boundary.js +328 -0
  12. package/dist/attribute/boundary.js.map +1 -0
  13. package/dist/attribute/call-site.d.ts +96 -0
  14. package/dist/attribute/call-site.js +183 -0
  15. package/dist/attribute/call-site.js.map +1 -0
  16. package/dist/attribute/component-hash.d.ts +188 -0
  17. package/dist/attribute/component-hash.js +312 -0
  18. package/dist/attribute/component-hash.js.map +1 -0
  19. package/dist/attribute/composition-fixture.d.ts +18 -0
  20. package/dist/attribute/composition-fixture.js +53 -0
  21. package/dist/attribute/composition-fixture.js.map +1 -0
  22. package/dist/attribute/composition.d.ts +182 -0
  23. package/dist/attribute/composition.js +169 -0
  24. package/dist/attribute/composition.js.map +1 -0
  25. package/dist/attribute/control.d.ts +43 -0
  26. package/dist/attribute/control.js +54 -0
  27. package/dist/attribute/control.js.map +1 -0
  28. package/dist/attribute/coverage.d.ts +99 -0
  29. package/dist/attribute/coverage.js +86 -0
  30. package/dist/attribute/coverage.js.map +1 -0
  31. package/dist/attribute/divergence.d.ts +102 -0
  32. package/dist/attribute/divergence.js +152 -0
  33. package/dist/attribute/divergence.js.map +1 -0
  34. package/dist/attribute/index.d.ts +43 -0
  35. package/dist/attribute/index.js +29 -0
  36. package/dist/attribute/index.js.map +1 -0
  37. package/dist/attribute/instability.d.ts +89 -0
  38. package/dist/attribute/instability.js +183 -0
  39. package/dist/attribute/instability.js.map +1 -0
  40. package/dist/attribute/instances.d.ts +145 -0
  41. package/dist/attribute/instances.js +140 -0
  42. package/dist/attribute/instances.js.map +1 -0
  43. package/dist/attribute/locate.d.ts +22 -0
  44. package/dist/attribute/locate.js +100 -0
  45. package/dist/attribute/locate.js.map +1 -0
  46. package/dist/attribute/mask.d.ts +156 -0
  47. package/dist/attribute/mask.js +201 -0
  48. package/dist/attribute/mask.js.map +1 -0
  49. package/dist/attribute/movement.d.ts +211 -0
  50. package/dist/attribute/movement.js +181 -0
  51. package/dist/attribute/movement.js.map +1 -0
  52. package/dist/attribute/region.d.ts +188 -0
  53. package/dist/attribute/region.js +144 -0
  54. package/dist/attribute/region.js.map +1 -0
  55. package/dist/attribute/source-map.d.ts +98 -0
  56. package/dist/attribute/source-map.js +264 -0
  57. package/dist/attribute/source-map.js.map +1 -0
  58. package/dist/attribute/source.d.ts +77 -0
  59. package/dist/attribute/source.js +95 -0
  60. package/dist/attribute/source.js.map +1 -0
  61. package/dist/attribute/stack.d.ts +80 -0
  62. package/dist/attribute/stack.js +108 -0
  63. package/dist/attribute/stack.js.map +1 -0
  64. package/dist/compare/band.d.ts +85 -0
  65. package/dist/compare/band.js +70 -0
  66. package/dist/compare/band.js.map +1 -0
  67. package/dist/compare/cascade.d.ts +45 -0
  68. package/dist/compare/cascade.js +71 -0
  69. package/dist/compare/cascade.js.map +1 -0
  70. package/dist/compare/derive.d.ts +78 -0
  71. package/dist/compare/derive.js +39 -0
  72. package/dist/compare/derive.js.map +1 -0
  73. package/dist/compare/diff/attribution.d.ts +23 -0
  74. package/dist/compare/diff/attribution.js +244 -0
  75. package/dist/compare/diff/attribution.js.map +1 -0
  76. package/dist/compare/diff/compare-nodes.d.ts +15 -0
  77. package/dist/compare/diff/compare-nodes.js +227 -0
  78. package/dist/compare/diff/compare-nodes.js.map +1 -0
  79. package/dist/compare/diff/components.d.ts +20 -0
  80. package/dist/compare/diff/components.js +76 -0
  81. package/dist/compare/diff/components.js.map +1 -0
  82. package/dist/compare/diff/delta.d.ts +165 -0
  83. package/dist/compare/diff/delta.js +12 -0
  84. package/dist/compare/diff/delta.js.map +1 -0
  85. package/dist/compare/diff/index.d.ts +69 -0
  86. package/dist/compare/diff/index.js +101 -0
  87. package/dist/compare/diff/index.js.map +1 -0
  88. package/dist/compare/diff/match.d.ts +37 -0
  89. package/dist/compare/diff/match.js +167 -0
  90. package/dist/compare/diff/match.js.map +1 -0
  91. package/dist/compare/explain.d.ts +23 -0
  92. package/dist/compare/explain.js +190 -0
  93. package/dist/compare/explain.js.map +1 -0
  94. package/dist/compare/holding-diff.d.ts +25 -0
  95. package/dist/compare/holding-diff.js +74 -0
  96. package/dist/compare/holding-diff.js.map +1 -0
  97. package/dist/compare/impact.d.ts +56 -0
  98. package/dist/compare/impact.js +124 -0
  99. package/dist/compare/impact.js.map +1 -0
  100. package/dist/compare/index.d.ts +30 -0
  101. package/dist/compare/index.js +22 -0
  102. package/dist/compare/index.js.map +1 -0
  103. package/dist/compare/instance.d.ts +30 -0
  104. package/dist/compare/instance.js +145 -0
  105. package/dist/compare/instance.js.map +1 -0
  106. package/dist/compare/observability.d.ts +49 -0
  107. package/dist/compare/observability.js +57 -0
  108. package/dist/compare/observability.js.map +1 -0
  109. package/dist/compare/parting.d.ts +179 -0
  110. package/dist/compare/parting.js +244 -0
  111. package/dist/compare/parting.js.map +1 -0
  112. package/dist/compare/slice.d.ts +74 -0
  113. package/dist/compare/slice.js +79 -0
  114. package/dist/compare/slice.js.map +1 -0
  115. package/dist/compare/value.d.ts +60 -0
  116. package/dist/compare/value.js +111 -0
  117. package/dist/compare/value.js.map +1 -0
  118. package/dist/format/accessibility.d.ts +27 -0
  119. package/dist/format/accessibility.js +17 -0
  120. package/dist/format/accessibility.js.map +1 -0
  121. package/dist/format/canonical.d.ts +38 -0
  122. package/dist/format/canonical.js +65 -0
  123. package/dist/format/canonical.js.map +1 -0
  124. package/dist/format/capture.d.ts +212 -0
  125. package/dist/format/capture.js +2 -0
  126. package/dist/format/capture.js.map +1 -0
  127. package/dist/format/document.d.ts +279 -0
  128. package/dist/format/document.js +65 -0
  129. package/dist/format/document.js.map +1 -0
  130. package/dist/format/environment.d.ts +120 -0
  131. package/dist/format/environment.js +53 -0
  132. package/dist/format/environment.js.map +1 -0
  133. package/dist/format/hash.d.ts +45 -0
  134. package/dist/format/hash.js +41 -0
  135. package/dist/format/hash.js.map +1 -0
  136. package/dist/format/holding.d.ts +134 -0
  137. package/dist/format/holding.js +2 -0
  138. package/dist/format/holding.js.map +1 -0
  139. package/dist/format/index.d.ts +39 -0
  140. package/dist/format/index.js +25 -0
  141. package/dist/format/index.js.map +1 -0
  142. package/dist/format/intervention.d.ts +87 -0
  143. package/dist/format/intervention.js +11 -0
  144. package/dist/format/intervention.js.map +1 -0
  145. package/dist/format/profile.d.ts +59 -0
  146. package/dist/format/profile.js +66 -0
  147. package/dist/format/profile.js.map +1 -0
  148. package/dist/format/provenance.d.ts +179 -0
  149. package/dist/format/provenance.js +222 -0
  150. package/dist/format/provenance.js.map +1 -0
  151. package/dist/format/sha256.d.ts +34 -0
  152. package/dist/format/sha256.js +152 -0
  153. package/dist/format/sha256.js.map +1 -0
  154. package/dist/format/snapshot.d.ts +263 -0
  155. package/dist/format/snapshot.js +2 -0
  156. package/dist/format/snapshot.js.map +1 -0
  157. package/dist/format/stabilize.d.ts +190 -0
  158. package/dist/format/stabilize.js +333 -0
  159. package/dist/format/stabilize.js.map +1 -0
  160. package/dist/format/tier.d.ts +19 -0
  161. package/dist/format/tier.js +11 -0
  162. package/dist/format/tier.js.map +1 -0
  163. package/dist/format/value.d.ts +115 -0
  164. package/dist/format/value.js +182 -0
  165. package/dist/format/value.js.map +1 -0
  166. package/dist/format/wiring.d.ts +115 -0
  167. package/dist/format/wiring.js +60 -0
  168. package/dist/format/wiring.js.map +1 -0
  169. package/dist/index.d.ts +35 -0
  170. package/dist/index.js +35 -0
  171. package/dist/index.js.map +1 -0
  172. package/dist/judge/docket.d.ts +85 -0
  173. package/dist/judge/docket.js +162 -0
  174. package/dist/judge/docket.js.map +1 -0
  175. package/dist/judge/fingerprint.d.ts +53 -0
  176. package/dist/judge/fingerprint.js +144 -0
  177. package/dist/judge/fingerprint.js.map +1 -0
  178. package/dist/judge/ignore.d.ts +206 -0
  179. package/dist/judge/ignore.js +237 -0
  180. package/dist/judge/ignore.js.map +1 -0
  181. package/dist/judge/index.d.ts +44 -0
  182. package/dist/judge/index.js +34 -0
  183. package/dist/judge/index.js.map +1 -0
  184. package/dist/judge/inspect.d.ts +148 -0
  185. package/dist/judge/inspect.js +208 -0
  186. package/dist/judge/inspect.js.map +1 -0
  187. package/dist/judge/intent.d.ts +146 -0
  188. package/dist/judge/intent.js +150 -0
  189. package/dist/judge/intent.js.map +1 -0
  190. package/dist/judge/locale.d.ts +111 -0
  191. package/dist/judge/locale.js +247 -0
  192. package/dist/judge/locale.js.map +1 -0
  193. package/dist/judge/scope.d.ts +41 -0
  194. package/dist/judge/scope.js +47 -0
  195. package/dist/judge/scope.js.map +1 -0
  196. package/dist/judge/sensitivity.d.ts +202 -0
  197. package/dist/judge/sensitivity.js +152 -0
  198. package/dist/judge/sensitivity.js.map +1 -0
  199. package/dist/judge/trail.d.ts +122 -0
  200. package/dist/judge/trail.js +96 -0
  201. package/dist/judge/trail.js.map +1 -0
  202. package/dist/judge/verdict.d.ts +39 -0
  203. package/dist/judge/verdict.js +32 -0
  204. package/dist/judge/verdict.js.map +1 -0
  205. package/dist/plan/index.d.ts +12 -0
  206. package/dist/plan/index.js +11 -0
  207. package/dist/plan/index.js.map +1 -0
  208. package/dist/plan/tool.d.ts +85 -0
  209. package/dist/plan/tool.js +87 -0
  210. package/dist/plan/tool.js.map +1 -0
  211. package/dist/relate/graph.d.ts +190 -0
  212. package/dist/relate/graph.js +234 -0
  213. package/dist/relate/graph.js.map +1 -0
  214. package/dist/relate/index.d.ts +16 -0
  215. package/dist/relate/index.js +16 -0
  216. package/dist/relate/index.js.map +1 -0
  217. package/dist/relate/merkle.d.ts +110 -0
  218. package/dist/relate/merkle.js +246 -0
  219. package/dist/relate/merkle.js.map +1 -0
  220. package/dist/relate/reach.d.ts +65 -0
  221. package/dist/relate/reach.js +111 -0
  222. package/dist/relate/reach.js.map +1 -0
  223. package/dist/relate/records.d.ts +129 -0
  224. package/dist/relate/records.js +112 -0
  225. package/dist/relate/records.js.map +1 -0
  226. package/dist/rules/index.d.ts +19 -0
  227. package/dist/rules/index.js +17 -0
  228. package/dist/rules/index.js.map +1 -0
  229. package/dist/rules/normalize/alias.d.ts +50 -0
  230. package/dist/rules/normalize/alias.js +111 -0
  231. package/dist/rules/normalize/alias.js.map +1 -0
  232. package/dist/rules/normalize/cascade.d.ts +85 -0
  233. package/dist/rules/normalize/cascade.js +324 -0
  234. package/dist/rules/normalize/cascade.js.map +1 -0
  235. package/dist/rules/normalize/color.d.ts +30 -0
  236. package/dist/rules/normalize/color.js +185 -0
  237. package/dist/rules/normalize/color.js.map +1 -0
  238. package/dist/rules/normalize/fixture.d.ts +83 -0
  239. package/dist/rules/normalize/fixture.js +74 -0
  240. package/dist/rules/normalize/fixture.js.map +1 -0
  241. package/dist/rules/normalize/index.d.ts +39 -0
  242. package/dist/rules/normalize/index.js +268 -0
  243. package/dist/rules/normalize/index.js.map +1 -0
  244. package/dist/rules/normalize/project.d.ts +42 -0
  245. package/dist/rules/normalize/project.js +83 -0
  246. package/dist/rules/normalize/project.js.map +1 -0
  247. package/dist/rules/normalize/shorthand.d.ts +38 -0
  248. package/dist/rules/normalize/shorthand.js +388 -0
  249. package/dist/rules/normalize/shorthand.js.map +1 -0
  250. package/dist/rules/normalize/value.d.ts +23 -0
  251. package/dist/rules/normalize/value.js +162 -0
  252. package/dist/rules/normalize/value.js.map +1 -0
  253. package/dist/rules/normalize/wrapper.d.ts +16 -0
  254. package/dist/rules/normalize/wrapper.js +137 -0
  255. package/dist/rules/normalize/wrapper.js.map +1 -0
  256. package/dist/rules/ruleset.d.ts +93 -0
  257. package/dist/rules/ruleset.js +175 -0
  258. package/dist/rules/ruleset.js.map +1 -0
  259. package/mark.svg +30 -0
  260. 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"]}