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