@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,167 @@
1
+ /**
2
+ * Identity key, most specific first.
3
+ *
4
+ * Ordering matters: a structural alias is the strongest signal available because
5
+ * it survives reordering and is stable across renders (that being the point of
6
+ * ADR-0003's aliasing). Role plus accessible name is next, since it is what a
7
+ * user of assistive technology would call the node. Owner chain plus tag comes
8
+ * third — two `<span>`s rendered by the same component are interchangeable in a
9
+ * way two spans from different components are not. Bare tag is the last resort.
10
+ */
11
+ function matchKey(node) {
12
+ if (node.alias !== undefined)
13
+ return `alias:${node.alias}`;
14
+ if (node.role !== undefined && node.name !== undefined) {
15
+ return `aria:${node.role}|${node.name}`;
16
+ }
17
+ if (node.provenance && node.provenance.owners.length > 0) {
18
+ const chain = node.provenance.owners.map((owner) => owner.name).join('>');
19
+ return `own:${chain}:${node.tag}`;
20
+ }
21
+ return `tag:${node.tag}`;
22
+ }
23
+ const SIGNATURES = new WeakMap();
24
+ /**
25
+ * What this subtree *says*, ignoring where it sits.
26
+ *
27
+ * Deliberately narrow: tag, the three ARIA fields, text, and the same of every
28
+ * descendant. Style is excluded, because a node that was restyled and a node
29
+ * that was replaced must not be confused — pairing by appearance would let a
30
+ * recolour look like a substitution. Position is excluded because that is the
31
+ * whole point.
32
+ *
33
+ * Cached per node: the recursion is O(subtree) and a parent computing it for
34
+ * every child at every level would be quadratic in depth.
35
+ */
36
+ function contentSignature(node) {
37
+ const cached = SIGNATURES.get(node);
38
+ if (cached !== undefined)
39
+ return cached;
40
+ const own = [node.tag, node.role ?? '', node.name ?? '', node.description ?? '', node.text ?? ''];
41
+ const signature = `${own.join('')}(${node.children.map(contentSignature).join('')})`;
42
+ SIGNATURES.set(node, signature);
43
+ return signature;
44
+ }
45
+ /**
46
+ * Content pass key. Scoped by identity so the refinement never crosses a
47
+ * boundary the identity key drew — two components rendering the same string are
48
+ * not the same node however identical their subtrees.
49
+ */
50
+ function contentKey(node) {
51
+ return `${matchKey(node)}\u0000${contentSignature(node)}`;
52
+ }
53
+ export function matchTrees(baseline, candidate) {
54
+ const pairs = new Map();
55
+ const added = [];
56
+ const removed = [];
57
+ const moved = new Set();
58
+ pairs.set(baseline, candidate);
59
+ matchChildren(baseline, candidate);
60
+ return { pairs, added, removed, moved };
61
+ function matchChildren(before, after) {
62
+ const byContent = index(before.children, contentKey);
63
+ const byIdentity = index(before.children, matchKey);
64
+ const taken = new Set();
65
+ /** Baseline index each candidate paired with, indexed by candidate position. */
66
+ const partner = Array.from({ length: after.children.length }, () => undefined);
67
+ // Pass 1 — identical content. A node that says the same thing is the same
68
+ // node, wherever it moved to.
69
+ after.children.forEach((child, afterIndex) => {
70
+ const claimed = claim(byContent.get(contentKey(child)), taken);
71
+ if (claimed !== undefined)
72
+ partner[afterIndex] = claimed;
73
+ });
74
+ // Pass 2 — whatever is left, by identity and then by order of appearance.
75
+ // This is what turns `[a, b, c]` → `[a, x, c]` into one changed string
76
+ // rather than one removal and one addition: `b` and `x` are the only two
77
+ // nodes their bucket has left, so they are each other's counterpart.
78
+ after.children.forEach((child, afterIndex) => {
79
+ if (partner[afterIndex] !== undefined)
80
+ return;
81
+ const claimed = claim(byIdentity.get(matchKey(child)), taken);
82
+ if (claimed !== undefined)
83
+ partner[afterIndex] = claimed;
84
+ });
85
+ const sequence = [];
86
+ after.children.forEach((child, afterIndex) => {
87
+ const beforeIndex = partner[afterIndex];
88
+ if (beforeIndex === undefined) {
89
+ // Unmatched candidates are recorded whole. Their descendants are not
90
+ // walked: a new subtree is one change to review, not one per node in it.
91
+ added.push(child);
92
+ return;
93
+ }
94
+ sequence.push(beforeIndex);
95
+ const counterpart = before.children[beforeIndex];
96
+ pairs.set(counterpart, child);
97
+ matchChildren(counterpart, child);
98
+ });
99
+ for (const beforeIndex of outOfOrder(sequence)) {
100
+ moved.add(before.children[beforeIndex]);
101
+ }
102
+ before.children.forEach((child, index) => {
103
+ if (!taken.has(index))
104
+ removed.push(child);
105
+ });
106
+ }
107
+ }
108
+ function index(children, key) {
109
+ const buckets = new Map();
110
+ children.forEach((child, position) => {
111
+ const bucket = buckets.get(key(child));
112
+ if (bucket)
113
+ bucket.push(position);
114
+ else
115
+ buckets.set(key(child), [position]);
116
+ });
117
+ return buckets;
118
+ }
119
+ /** Lowest unclaimed index in a bucket, marking it claimed. */
120
+ function claim(bucket, taken) {
121
+ const free = bucket?.find((position) => !taken.has(position));
122
+ if (free !== undefined)
123
+ taken.add(free);
124
+ return free;
125
+ }
126
+ /**
127
+ * Indices that must have moved for `sequence` to be in this order.
128
+ *
129
+ * The complement of a longest increasing subsequence: everything in that
130
+ * subsequence kept its relative order and needs no explanation, and what is left
131
+ * is the smallest set of movements that produces the observed sequence. A
132
+ * prepend leaves the sequence increasing and therefore reports nothing moved,
133
+ * which is the answer `prepend/list` asks for — one added node, and not three
134
+ * displaced siblings alongside it.
135
+ */
136
+ function outOfOrder(sequence) {
137
+ if (sequence.length < 2)
138
+ return [];
139
+ // Patience sorting. `tails[k]` is the smallest tail of an increasing run of
140
+ // length k+1; `previous` reconstructs which elements formed the longest one.
141
+ const tails = [];
142
+ const tailIndex = [];
143
+ const previous = Array.from({ length: sequence.length }, () => undefined);
144
+ for (let position = 0; position < sequence.length; position += 1) {
145
+ const value = sequence[position];
146
+ let low = 0;
147
+ let high = tails.length;
148
+ while (low < high) {
149
+ const mid = (low + high) >> 1;
150
+ if (tails[mid] < value)
151
+ low = mid + 1;
152
+ else
153
+ high = mid;
154
+ }
155
+ tails[low] = value;
156
+ tailIndex[low] = position;
157
+ previous[position] = low > 0 ? tailIndex[low - 1] : undefined;
158
+ }
159
+ const kept = new Set();
160
+ let cursor = tailIndex[tails.length - 1];
161
+ while (cursor !== undefined) {
162
+ kept.add(cursor);
163
+ cursor = previous[cursor];
164
+ }
165
+ return sequence.filter((_, position) => !kept.has(position));
166
+ }
167
+ //# sourceMappingURL=match.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.js","sourceRoot":"","sources":["../../../src/compare/diff/match.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SemanticNode } from '../../format/snapshot.js';\n\n/**\n * Node correspondence.\n *\n * Paths are positional, so inserting one sibling renumbers everything after it.\n * Diffing by path alone would report a single insertion as \"every following node\n * changed\" — the classic snapshot-test failure that trains people to stop\n * reading diffs. Matching therefore happens by identity first, content second,\n * and position last.\n *\n * The middle pass is the one that took three attempts to find. Identity keys\n * collapse whenever a node has no alias, no accessible name and no provenance —\n * five bare `<li>`s bucket together as `tag:li` — and pairing inside that bucket\n * by position turns a rotation into \"every item's text changed\". The corpus case\n * `reorder/list` was written to catch exactly that and could not: its assertions\n * were the verdict and the root count, and both are correct while the report is\n * useless. Scoring the *band* is what exposed it — five `text-changed` deltas\n * band as `content` where a rotation bands as `geometry`.\n */\n\nexport interface Matching {\n /** Baseline node → candidate node. */\n readonly pairs: ReadonlyMap<SemanticNode, SemanticNode>;\n readonly added: readonly SemanticNode[];\n readonly removed: readonly SemanticNode[];\n /**\n * Pairs whose position among their siblings changed *relative to each other*.\n *\n * Not \"whose index differs\". Prepending one item shifts every index after it\n * without reordering anything, and reporting three moves for one insertion is\n * the same fatigue that positional diffing produces. The moved set is the\n * complement of the longest run that stayed in order, which is the smallest\n * set of nodes whose movement explains the new sequence.\n */\n readonly moved: ReadonlySet<SemanticNode>;\n}\n\n/**\n * Identity key, most specific first.\n *\n * Ordering matters: a structural alias is the strongest signal available because\n * it survives reordering and is stable across renders (that being the point of\n * ADR-0003's aliasing). Role plus accessible name is next, since it is what a\n * user of assistive technology would call the node. Owner chain plus tag comes\n * third — two `<span>`s rendered by the same component are interchangeable in a\n * way two spans from different components are not. Bare tag is the last resort.\n */\nfunction matchKey(node: SemanticNode): string {\n if (node.alias !== undefined) return `alias:${node.alias}`;\n if (node.role !== undefined && node.name !== undefined) {\n return `aria:${node.role}|${node.name}`;\n }\n if (node.provenance && node.provenance.owners.length > 0) {\n const chain = node.provenance.owners.map((owner) => owner.name).join('>');\n return `own:${chain}:${node.tag}`;\n }\n return `tag:${node.tag}`;\n}\n\nconst SIGNATURES = new WeakMap<SemanticNode, string>();\n\n/**\n * What this subtree *says*, ignoring where it sits.\n *\n * Deliberately narrow: tag, the three ARIA fields, text, and the same of every\n * descendant. Style is excluded, because a node that was restyled and a node\n * that was replaced must not be confused — pairing by appearance would let a\n * recolour look like a substitution. Position is excluded because that is the\n * whole point.\n *\n * Cached per node: the recursion is O(subtree) and a parent computing it for\n * every child at every level would be quadratic in depth.\n */\nfunction contentSignature(node: SemanticNode): string {\n const cached = SIGNATURES.get(node);\n if (cached !== undefined) return cached;\n\n const own = [node.tag, node.role ?? '', node.name ?? '', node.description ?? '', node.text ?? ''];\n const signature = `${own.join('\u0001')}(${node.children.map(contentSignature).join('\u0002')})`;\n\n SIGNATURES.set(node, signature);\n return signature;\n}\n\n/**\n * Content pass key. Scoped by identity so the refinement never crosses a\n * boundary the identity key drew — two components rendering the same string are\n * not the same node however identical their subtrees.\n */\nfunction contentKey(node: SemanticNode): string {\n return `${matchKey(node)}\\u0000${contentSignature(node)}`;\n}\n\nexport function matchTrees(baseline: SemanticNode, candidate: SemanticNode): Matching {\n const pairs = new Map<SemanticNode, SemanticNode>();\n const added: SemanticNode[] = [];\n const removed: SemanticNode[] = [];\n const moved = new Set<SemanticNode>();\n\n pairs.set(baseline, candidate);\n matchChildren(baseline, candidate);\n\n return { pairs, added, removed, moved };\n\n function matchChildren(before: SemanticNode, after: SemanticNode): void {\n const byContent = index(before.children, contentKey);\n const byIdentity = index(before.children, matchKey);\n const taken = new Set<number>();\n\n /** Baseline index each candidate paired with, indexed by candidate position. */\n const partner: (number | undefined)[] = Array.from(\n { length: after.children.length },\n () => undefined,\n );\n\n // Pass 1 — identical content. A node that says the same thing is the same\n // node, wherever it moved to.\n after.children.forEach((child, afterIndex) => {\n const claimed = claim(byContent.get(contentKey(child)), taken);\n if (claimed !== undefined) partner[afterIndex] = claimed;\n });\n\n // Pass 2 — whatever is left, by identity and then by order of appearance.\n // This is what turns `[a, b, c]` → `[a, x, c]` into one changed string\n // rather than one removal and one addition: `b` and `x` are the only two\n // nodes their bucket has left, so they are each other's counterpart.\n after.children.forEach((child, afterIndex) => {\n if (partner[afterIndex] !== undefined) return;\n const claimed = claim(byIdentity.get(matchKey(child)), taken);\n if (claimed !== undefined) partner[afterIndex] = claimed;\n });\n\n const sequence: number[] = [];\n after.children.forEach((child, afterIndex) => {\n const beforeIndex = partner[afterIndex];\n if (beforeIndex === undefined) {\n // Unmatched candidates are recorded whole. Their descendants are not\n // walked: a new subtree is one change to review, not one per node in it.\n added.push(child);\n return;\n }\n\n sequence.push(beforeIndex);\n const counterpart = before.children[beforeIndex]!;\n pairs.set(counterpart, child);\n matchChildren(counterpart, child);\n });\n\n for (const beforeIndex of outOfOrder(sequence)) {\n moved.add(before.children[beforeIndex]!);\n }\n\n before.children.forEach((child, index) => {\n if (!taken.has(index)) removed.push(child);\n });\n }\n}\n\nfunction index(\n children: readonly SemanticNode[],\n key: (node: SemanticNode) => string,\n): Map<string, number[]> {\n const buckets = new Map<string, number[]>();\n\n children.forEach((child, position) => {\n const bucket = buckets.get(key(child));\n if (bucket) bucket.push(position);\n else buckets.set(key(child), [position]);\n });\n\n return buckets;\n}\n\n/** Lowest unclaimed index in a bucket, marking it claimed. */\nfunction claim(bucket: number[] | undefined, taken: Set<number>): number | undefined {\n const free = bucket?.find((position) => !taken.has(position));\n if (free !== undefined) taken.add(free);\n return free;\n}\n\n/**\n * Indices that must have moved for `sequence` to be in this order.\n *\n * The complement of a longest increasing subsequence: everything in that\n * subsequence kept its relative order and needs no explanation, and what is left\n * is the smallest set of movements that produces the observed sequence. A\n * prepend leaves the sequence increasing and therefore reports nothing moved,\n * which is the answer `prepend/list` asks for — one added node, and not three\n * displaced siblings alongside it.\n */\nfunction outOfOrder(sequence: readonly number[]): readonly number[] {\n if (sequence.length < 2) return [];\n\n // Patience sorting. `tails[k]` is the smallest tail of an increasing run of\n // length k+1; `previous` reconstructs which elements formed the longest one.\n const tails: number[] = [];\n const tailIndex: number[] = [];\n const previous: (number | undefined)[] = Array.from(\n { length: sequence.length },\n () => undefined,\n );\n\n for (let position = 0; position < sequence.length; position += 1) {\n const value = sequence[position]!;\n\n let low = 0;\n let high = tails.length;\n while (low < high) {\n const mid = (low + high) >> 1;\n if (tails[mid]! < value) low = mid + 1;\n else high = mid;\n }\n\n tails[low] = value;\n tailIndex[low] = position;\n previous[position] = low > 0 ? tailIndex[low - 1] : undefined;\n }\n\n const kept = new Set<number>();\n let cursor: number | undefined = tailIndex[tails.length - 1];\n while (cursor !== undefined) {\n kept.add(cursor);\n cursor = previous[cursor];\n }\n\n return sequence.filter((_, position) => !kept.has(position));\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import type { Parting } from './parting.js';
2
+ /**
3
+ * A parting, spoken.
4
+ *
5
+ * The rung a run reached is only worth what a reader takes from it, and the
6
+ * three sentences below are three different products:
7
+ *
8
+ * ```text
9
+ * a <div> rendered <p> on one side and <span> on the other
10
+ * Summary was handed a different `total`
11
+ * Cart chose differently — useState #2 moved, and Summary's `total` follows
12
+ * ```
13
+ *
14
+ * The first is a diff. The second localises. Only the third names a cause, and
15
+ * it is the only one somebody can act on without opening the component and
16
+ * guessing. So the shape here is an **origin and its manifestations**, not a
17
+ * flat list: an origin line, then the boundaries and deltas downstream of it,
18
+ * indented. A collateral boundary with no origin above it gets a line of its own
19
+ * — that is a parting this run could not trace further, and hiding it under
20
+ * something else would overstate what was found.
21
+ */
22
+ export declare function explainParting(parting: Parting): readonly string[];
23
+ //# sourceMappingURL=explain.d.ts.map
@@ -0,0 +1,190 @@
1
+ /**
2
+ * A parting, spoken.
3
+ *
4
+ * The rung a run reached is only worth what a reader takes from it, and the
5
+ * three sentences below are three different products:
6
+ *
7
+ * ```text
8
+ * a <div> rendered <p> on one side and <span> on the other
9
+ * Summary was handed a different `total`
10
+ * Cart chose differently — useState #2 moved, and Summary's `total` follows
11
+ * ```
12
+ *
13
+ * The first is a diff. The second localises. Only the third names a cause, and
14
+ * it is the only one somebody can act on without opening the component and
15
+ * guessing. So the shape here is an **origin and its manifestations**, not a
16
+ * flat list: an origin line, then the boundaries and deltas downstream of it,
17
+ * indented. A collateral boundary with no origin above it gets a line of its own
18
+ * — that is a parting this run could not trace further, and hiding it under
19
+ * something else would overstate what was found.
20
+ */
21
+ export function explainParting(parting) {
22
+ const boundaries = parting.boundaries;
23
+ const headline = slice(parting.slice);
24
+ if (boundaries === undefined) {
25
+ return [headline, ' no framework boundary was read, so nothing can be said about why'];
26
+ }
27
+ if (boundaries.length === 0) {
28
+ return [headline, ' every boundary read held its inputs and its output'];
29
+ }
30
+ const origins = parting.origins ?? [];
31
+ const claimed = new Set();
32
+ const lines = [headline];
33
+ for (const origin of origins) {
34
+ if (claimed.has(origin))
35
+ continue;
36
+ claimed.add(origin);
37
+ const downstream = boundaries.filter((other) => !claimed.has(other) && under(origin.path, other.path));
38
+ for (const other of downstream)
39
+ claimed.add(other);
40
+ lines.push(sentence(origin));
41
+ lines.push(...deltaLines(origin, boundaries));
42
+ // The avalanche. One input at a fork can put a boundary on every component
43
+ // beneath it, and enumerating them buries the one line worth reading under
44
+ // its own consequences. Past the cap the fan-out is stated as a size —
45
+ // pointing at the fork is the finding, and the boundary list is still there
46
+ // for a caller that wants to walk it.
47
+ if (downstream.length > FANOUT) {
48
+ const deltas = downstream.reduce((total, other) => total + other.deltas, 0);
49
+ lines.push(` manifests across ${downstream.length} boundaries below it` +
50
+ (deltas > 0 ? `, ${deltas} delta${deltas === 1 ? '' : 's'} in all` : ''));
51
+ }
52
+ else {
53
+ for (const other of downstream) {
54
+ lines.push(` manifests as ${sentence(other)}`);
55
+ lines.push(...deltaLines(other, boundaries).map((line) => ` ${line}`));
56
+ }
57
+ }
58
+ }
59
+ for (const rest of boundaries) {
60
+ if (claimed.has(rest))
61
+ continue;
62
+ lines.push(sentence(rest));
63
+ lines.push(...deltaLines(rest, boundaries));
64
+ }
65
+ return lines;
66
+ }
67
+ function sentence(boundary) {
68
+ const { component, inputs } = boundary;
69
+ switch (boundary.rung) {
70
+ case 'stateful':
71
+ return `${component} chose differently — ${list(inputs.filter(isHook))} moved`;
72
+ case 'external':
73
+ return `${component} read a different external store — ${list(inputs.filter(isHook))} moved`;
74
+ case 'provided':
75
+ return `${component} was given a different ${list(inputs.filter((i) => i.kind === 'context'))}`;
76
+ case 'handed':
77
+ return `${component} was handed a different ${list(inputs.filter((i) => i.kind === 'prop'))}`;
78
+ // Phrased at the ancestor rather than at the component, because that is
79
+ // where the reader has to go. `Price` did nothing; something it is standing
80
+ // inside declared a value it never declares for itself.
81
+ case 'inherited':
82
+ return (`${component} inherited a different ` +
83
+ `${list(inputs.filter((i) => i.kind === 'inherited'))} — an ancestor declared it`);
84
+ case 'undetermined':
85
+ return `${component} rendered differently from inputs that all agreed — nondeterministic`;
86
+ case 'unread':
87
+ return `${component} rendered differently, and what it holds could not be read`;
88
+ case 'unpaired':
89
+ return `${component} is present on one side only`;
90
+ }
91
+ }
92
+ /** What `holdingOf` writes for a context React could not name. */
93
+ const ANONYMOUS = '(anonymous)';
94
+ function isHook(input) {
95
+ return input.kind === 'hook';
96
+ }
97
+ /**
98
+ * Name the moved inputs, hooks by call position.
99
+ *
100
+ * `useState #2` rather than `useState` because a component with four `useState`
101
+ * calls is the ordinary case, and a sentence naming the hook without saying
102
+ * which one sends the reader back to counting — the work this module exists to
103
+ * have already done.
104
+ */
105
+ function list(inputs) {
106
+ const named = inputs.map((input) => {
107
+ if (input.index !== undefined)
108
+ return `${input.name} #${input.index}`;
109
+ // A `createContext` call with no `displayName` is the common case, and
110
+ // "a different `(anonymous)`" names the gap in React's metadata rather than
111
+ // the thing that moved.
112
+ return input.name === ANONYMOUS ? 'context value' : `\`${input.name}\``;
113
+ });
114
+ if (named.length === 0)
115
+ return 'input';
116
+ if (named.length === 1)
117
+ return named[0];
118
+ return `${named.slice(0, -1).join(', ')} and ${named[named.length - 1]}`;
119
+ }
120
+ /**
121
+ * The deltas at a boundary, and the one case worth saying nothing about.
122
+ *
123
+ * A boundary owning no delta is only *quiet* if nothing under it owns one
124
+ * either. Otherwise the deltas are real and belong to a nested boundary, whose
125
+ * own line already reports them — and "and rendered the same anyway" directly
126
+ * beneath "chose differently" reads as a contradiction rather than as the
127
+ * ownership statement it is.
128
+ */
129
+ function deltaLines(boundary, all) {
130
+ if (boundary.deltas === 0) {
131
+ const moved = all.some((other) => under(boundary.path, other.path) && other.deltas > 0);
132
+ return moved ? [] : [' and rendered the same anyway'];
133
+ }
134
+ const bands = boundary.bands.join(', ');
135
+ const count = `${boundary.deltas} delta${boundary.deltas === 1 ? '' : 's'} here (${bands})`;
136
+ return [` ${count}${named(boundary.moved)}`];
137
+ }
138
+ const SHOWN = 4;
139
+ /** Above this many boundaries under one origin, the fan-out is the finding. */
140
+ const FANOUT = 3;
141
+ /**
142
+ * The triage line, first, before anything about which input moved.
143
+ *
144
+ * Deliberately a whole sentence rather than the bare word: `refactor` alone
145
+ * reads as a label somebody applied, and the clause is the evidence for it.
146
+ */
147
+ function slice(kind) {
148
+ switch (kind) {
149
+ case 'settled':
150
+ return 'settled — the component tree, its inputs and its output all held';
151
+ case 'variation':
152
+ return 'variation — an input moved and the page followed';
153
+ case 'flake':
154
+ return 'flake — every input agreed, the component tree held, and the page moved anyway';
155
+ case 'reshaped':
156
+ return 'reshaped — the component tree is a different tree and the page followed';
157
+ case 'refactor':
158
+ return 'refactor — the component tree moved and the page did not';
159
+ case 'absorbed':
160
+ return 'absorbed — an input moved and the page did not';
161
+ case 'unread':
162
+ return 'unread — the page moved and what would explain it was not read';
163
+ }
164
+ }
165
+ /**
166
+ * The properties that moved, truncated where a list stops being a sentence.
167
+ *
168
+ * Four because `padding` expands to four longhands and a reader who has seen
169
+ * the first learns nothing from the rest. What was dropped is counted rather
170
+ * than elided — a list that quietly ends reads as the whole list.
171
+ */
172
+ function named(moved) {
173
+ if (moved === undefined || moved.length === 0)
174
+ return '';
175
+ if (moved.length <= SHOWN)
176
+ return ` — ${moved.join(', ')}`;
177
+ return ` — ${moved.slice(0, SHOWN).join(', ')} and ${moved.length - SHOWN} more`;
178
+ }
179
+ /**
180
+ * Whether one path encloses another, strictly.
181
+ *
182
+ * Segment-aware: `0/1` must not swallow `0/10`, and a plain `startsWith` would
183
+ * make every tenth sibling collateral of its second.
184
+ */
185
+ function under(ancestor, path) {
186
+ if (ancestor === path)
187
+ return false;
188
+ return ancestor === '' || path.startsWith(`${ancestor}/`);
189
+ }
190
+ //# sourceMappingURL=explain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explain.js","sourceRoot":"","sources":["../../src/compare/explain.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Parting, PartedBoundary, MovedInput } from './parting.js';\nimport type { PartingSlice } from './slice.js';\n\n/**\n * A parting, spoken.\n *\n * The rung a run reached is only worth what a reader takes from it, and the\n * three sentences below are three different products:\n *\n * ```text\n * a <div> rendered <p> on one side and <span> on the other\n * Summary was handed a different `total`\n * Cart chose differently — useState #2 moved, and Summary's `total` follows\n * ```\n *\n * The first is a diff. The second localises. Only the third names a cause, and\n * it is the only one somebody can act on without opening the component and\n * guessing. So the shape here is an **origin and its manifestations**, not a\n * flat list: an origin line, then the boundaries and deltas downstream of it,\n * indented. A collateral boundary with no origin above it gets a line of its own\n * — that is a parting this run could not trace further, and hiding it under\n * something else would overstate what was found.\n */\nexport function explainParting(parting: Parting): readonly string[] {\n const boundaries = parting.boundaries;\n const headline = slice(parting.slice);\n\n if (boundaries === undefined) {\n return [headline, ' no framework boundary was read, so nothing can be said about why'];\n }\n if (boundaries.length === 0) {\n return [headline, ' every boundary read held its inputs and its output'];\n }\n\n const origins = parting.origins ?? [];\n const claimed = new Set<PartedBoundary>();\n const lines: string[] = [headline];\n\n for (const origin of origins) {\n if (claimed.has(origin)) continue;\n claimed.add(origin);\n const downstream = boundaries.filter(\n (other) => !claimed.has(other) && under(origin.path, other.path),\n );\n for (const other of downstream) claimed.add(other);\n\n lines.push(sentence(origin));\n lines.push(...deltaLines(origin, boundaries));\n\n // The avalanche. One input at a fork can put a boundary on every component\n // beneath it, and enumerating them buries the one line worth reading under\n // its own consequences. Past the cap the fan-out is stated as a size —\n // pointing at the fork is the finding, and the boundary list is still there\n // for a caller that wants to walk it.\n if (downstream.length > FANOUT) {\n const deltas = downstream.reduce((total, other) => total + other.deltas, 0);\n lines.push(\n ` manifests across ${downstream.length} boundaries below it` +\n (deltas > 0 ? `, ${deltas} delta${deltas === 1 ? '' : 's'} in all` : ''),\n );\n } else {\n for (const other of downstream) {\n lines.push(` manifests as ${sentence(other)}`);\n lines.push(...deltaLines(other, boundaries).map((line) => ` ${line}`));\n }\n }\n }\n\n for (const rest of boundaries) {\n if (claimed.has(rest)) continue;\n lines.push(sentence(rest));\n lines.push(...deltaLines(rest, boundaries));\n }\n\n return lines;\n}\n\nfunction sentence(boundary: PartedBoundary): string {\n const { component, inputs } = boundary;\n\n switch (boundary.rung) {\n case 'stateful':\n return `${component} chose differently — ${list(inputs.filter(isHook))} moved`;\n case 'external':\n return `${component} read a different external store — ${list(inputs.filter(isHook))} moved`;\n case 'provided':\n return `${component} was given a different ${list(inputs.filter((i) => i.kind === 'context'))}`;\n case 'handed':\n return `${component} was handed a different ${list(inputs.filter((i) => i.kind === 'prop'))}`;\n // Phrased at the ancestor rather than at the component, because that is\n // where the reader has to go. `Price` did nothing; something it is standing\n // inside declared a value it never declares for itself.\n case 'inherited':\n return (\n `${component} inherited a different ` +\n `${list(inputs.filter((i) => i.kind === 'inherited'))} — an ancestor declared it`\n );\n case 'undetermined':\n return `${component} rendered differently from inputs that all agreed — nondeterministic`;\n case 'unread':\n return `${component} rendered differently, and what it holds could not be read`;\n case 'unpaired':\n return `${component} is present on one side only`;\n }\n}\n\n/** What `holdingOf` writes for a context React could not name. */\nconst ANONYMOUS = '(anonymous)';\n\nfunction isHook(input: MovedInput): boolean {\n return input.kind === 'hook';\n}\n\n/**\n * Name the moved inputs, hooks by call position.\n *\n * `useState #2` rather than `useState` because a component with four `useState`\n * calls is the ordinary case, and a sentence naming the hook without saying\n * which one sends the reader back to counting — the work this module exists to\n * have already done.\n */\nfunction list(inputs: readonly MovedInput[]): string {\n const named = inputs.map((input) => {\n if (input.index !== undefined) return `${input.name} #${input.index}`;\n // A `createContext` call with no `displayName` is the common case, and\n // \"a different `(anonymous)`\" names the gap in React's metadata rather than\n // the thing that moved.\n return input.name === ANONYMOUS ? 'context value' : `\\`${input.name}\\``;\n });\n if (named.length === 0) return 'input';\n if (named.length === 1) return named[0] as string;\n return `${named.slice(0, -1).join(', ')} and ${named[named.length - 1] as string}`;\n}\n\n/**\n * The deltas at a boundary, and the one case worth saying nothing about.\n *\n * A boundary owning no delta is only *quiet* if nothing under it owns one\n * either. Otherwise the deltas are real and belong to a nested boundary, whose\n * own line already reports them — and \"and rendered the same anyway\" directly\n * beneath \"chose differently\" reads as a contradiction rather than as the\n * ownership statement it is.\n */\nfunction deltaLines(\n boundary: PartedBoundary,\n all: readonly PartedBoundary[],\n): readonly string[] {\n if (boundary.deltas === 0) {\n const moved = all.some((other) => under(boundary.path, other.path) && other.deltas > 0);\n return moved ? [] : [' and rendered the same anyway'];\n }\n const bands = boundary.bands.join(', ');\n const count = `${boundary.deltas} delta${boundary.deltas === 1 ? '' : 's'} here (${bands})`;\n return [` ${count}${named(boundary.moved)}`];\n}\n\nconst SHOWN = 4;\n\n/** Above this many boundaries under one origin, the fan-out is the finding. */\nconst FANOUT = 3;\n\n/**\n * The triage line, first, before anything about which input moved.\n *\n * Deliberately a whole sentence rather than the bare word: `refactor` alone\n * reads as a label somebody applied, and the clause is the evidence for it.\n */\nfunction slice(kind: PartingSlice): string {\n switch (kind) {\n case 'settled':\n return 'settled — the component tree, its inputs and its output all held';\n case 'variation':\n return 'variation — an input moved and the page followed';\n case 'flake':\n return 'flake — every input agreed, the component tree held, and the page moved anyway';\n case 'reshaped':\n return 'reshaped — the component tree is a different tree and the page followed';\n case 'refactor':\n return 'refactor — the component tree moved and the page did not';\n case 'absorbed':\n return 'absorbed — an input moved and the page did not';\n case 'unread':\n return 'unread — the page moved and what would explain it was not read';\n }\n}\n\n/**\n * The properties that moved, truncated where a list stops being a sentence.\n *\n * Four because `padding` expands to four longhands and a reader who has seen\n * the first learns nothing from the rest. What was dropped is counted rather\n * than elided — a list that quietly ends reads as the whole list.\n */\nfunction named(moved: readonly string[] | undefined): string {\n if (moved === undefined || moved.length === 0) return '';\n if (moved.length <= SHOWN) return ` — ${moved.join(', ')}`;\n return ` — ${moved.slice(0, SHOWN).join(', ')} and ${moved.length - SHOWN} more`;\n}\n\n/**\n * Whether one path encloses another, strictly.\n *\n * Segment-aware: `0/1` must not swallow `0/10`, and a plain `startsWith` would\n * make every tenth sibling collateral of its second.\n */\nfunction under(ancestor: string, path: string): boolean {\n if (ancestor === path) return false;\n return ancestor === '' || path.startsWith(`${ancestor}/`);\n}\n"]}
@@ -0,0 +1,25 @@
1
+ import type { Holding } from '../format/holding.js';
2
+ import type { MovedInput } from './parting.js';
3
+ /**
4
+ * What a boundary received, as a framework adapter read it.
5
+ *
6
+ * The counterpart to `cascade.ts`: that one reads the input nobody passed, and
7
+ * this one reads the three that were — props, contexts and hook cells. Split out
8
+ * because they fail in opposite directions and the difference is the whole
9
+ * safety property. A cascade reading is either present or the property was
10
+ * declared; a holding reading can be *absent*, and absent must never settle as
11
+ * agreement. Everything here that returns `unread` is doing that work.
12
+ */
13
+ /**
14
+ * What differs between two readings of one boundary.
15
+ *
16
+ * `unread` is returned beside the inputs rather than folded into them because
17
+ * the two answer different questions. The inputs say what moved; `unread` says
18
+ * whether "nothing moved" is a reading or a shrug, and only the second caller —
19
+ * the one about to call a render nondeterministic — needs it.
20
+ */
21
+ export declare function compareHoldings(left: Holding | undefined, right: Holding | undefined): {
22
+ inputs: readonly MovedInput[];
23
+ unread: boolean;
24
+ };
25
+ //# sourceMappingURL=holding-diff.d.ts.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * What a boundary received, as a framework adapter read it.
3
+ *
4
+ * The counterpart to `cascade.ts`: that one reads the input nobody passed, and
5
+ * this one reads the three that were — props, contexts and hook cells. Split out
6
+ * because they fail in opposite directions and the difference is the whole
7
+ * safety property. A cascade reading is either present or the property was
8
+ * declared; a holding reading can be *absent*, and absent must never settle as
9
+ * agreement. Everything here that returns `unread` is doing that work.
10
+ */
11
+ /**
12
+ * What differs between two readings of one boundary.
13
+ *
14
+ * `unread` is returned beside the inputs rather than folded into them because
15
+ * the two answer different questions. The inputs say what moved; `unread` says
16
+ * whether "nothing moved" is a reading or a shrug, and only the second caller —
17
+ * the one about to call a render nondeterministic — needs it.
18
+ */
19
+ export function compareHoldings(left, right) {
20
+ const inputs = [];
21
+ let unread = left?.unread !== undefined || right?.unread !== undefined;
22
+ // Absent props mean `memoizedProps` was not an object, which is a failure to
23
+ // read rather than a component with no props. Symmetric absence is left alone:
24
+ // two readings that failed the same way have not disagreed.
25
+ if ((left?.props === undefined) !== (right?.props === undefined))
26
+ unread = true;
27
+ else
28
+ compareNamed('prop', left?.props, right?.props, inputs);
29
+ // Contexts are different: absent means `dependencies` was null, and that is a
30
+ // positive reading — this component subscribes to no context.
31
+ compareNamed('context', left?.contexts, right?.contexts, inputs);
32
+ if (left?.cells === undefined || right?.cells === undefined)
33
+ unread = true;
34
+ else
35
+ compareCells(left.cells, right.cells, inputs);
36
+ return { inputs, unread };
37
+ }
38
+ function compareNamed(kind, left, right, into) {
39
+ const before = new Map((left ?? []).map((value) => [value.name, value.digest]));
40
+ const after = new Map((right ?? []).map((value) => [value.name, value.digest]));
41
+ for (const name of [...new Set([...before.keys(), ...after.keys()])].sort()) {
42
+ const from = before.get(name);
43
+ const to = after.get(name);
44
+ if (from !== to)
45
+ into.push({ kind, name, from, to });
46
+ }
47
+ }
48
+ /**
49
+ * Hook cells, joined on call position.
50
+ *
51
+ * On position rather than on name because position is what a hook *is* to React
52
+ * — the rule the linter enforces — and because a component that ran a different
53
+ * number of hooks took a different branch before it rendered anything. That case
54
+ * arrives here as cells present on one side only, which is a reading rather than
55
+ * a join failure, and one of the loudest available.
56
+ */
57
+ function compareCells(left, right, into) {
58
+ const before = new Map(left.map((cell) => [cell.index, cell]));
59
+ const after = new Map(right.map((cell) => [cell.index, cell]));
60
+ for (const index of [...new Set([...before.keys(), ...after.keys()])].sort((a, b) => a - b)) {
61
+ const from = before.get(index);
62
+ const to = after.get(index);
63
+ if (from?.digest === to?.digest && from?.hook === to?.hook)
64
+ continue;
65
+ into.push({
66
+ kind: 'hook',
67
+ name: to?.hook ?? from?.hook ?? '(unknown)',
68
+ index,
69
+ from: from?.digest,
70
+ to: to?.digest,
71
+ });
72
+ }
73
+ }
74
+ //# sourceMappingURL=holding-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"holding-diff.js","sourceRoot":"","sources":["../../src/compare/holding-diff.ts"],"names":[],"mappings":"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","sourcesContent":["import type { HeldCell, HeldValue, Holding } from '../format/holding.js';\nimport type { MovedInput } from './parting.js';\n\n/**\n * What a boundary received, as a framework adapter read it.\n *\n * The counterpart to `cascade.ts`: that one reads the input nobody passed, and\n * this one reads the three that were — props, contexts and hook cells. Split out\n * because they fail in opposite directions and the difference is the whole\n * safety property. A cascade reading is either present or the property was\n * declared; a holding reading can be *absent*, and absent must never settle as\n * agreement. Everything here that returns `unread` is doing that work.\n */\n\n/**\n * What differs between two readings of one boundary.\n *\n * `unread` is returned beside the inputs rather than folded into them because\n * the two answer different questions. The inputs say what moved; `unread` says\n * whether \"nothing moved\" is a reading or a shrug, and only the second caller —\n * the one about to call a render nondeterministic — needs it.\n */\nexport function compareHoldings(\n left: Holding | undefined,\n right: Holding | undefined,\n): { inputs: readonly MovedInput[]; unread: boolean } {\n const inputs: MovedInput[] = [];\n let unread = left?.unread !== undefined || right?.unread !== undefined;\n\n // Absent props mean `memoizedProps` was not an object, which is a failure to\n // read rather than a component with no props. Symmetric absence is left alone:\n // two readings that failed the same way have not disagreed.\n if ((left?.props === undefined) !== (right?.props === undefined)) unread = true;\n else compareNamed('prop', left?.props, right?.props, inputs);\n\n // Contexts are different: absent means `dependencies` was null, and that is a\n // positive reading — this component subscribes to no context.\n compareNamed('context', left?.contexts, right?.contexts, inputs);\n\n if (left?.cells === undefined || right?.cells === undefined) unread = true;\n else compareCells(left.cells, right.cells, inputs);\n\n return { inputs, unread };\n}\n\nfunction compareNamed(\n kind: 'prop' | 'context',\n left: readonly HeldValue[] | undefined,\n right: readonly HeldValue[] | undefined,\n into: MovedInput[],\n): void {\n const before = new Map((left ?? []).map((value) => [value.name, value.digest]));\n const after = new Map((right ?? []).map((value) => [value.name, value.digest]));\n\n for (const name of [...new Set([...before.keys(), ...after.keys()])].sort()) {\n const from = before.get(name);\n const to = after.get(name);\n if (from !== to) into.push({ kind, name, from, to });\n }\n}\n\n/**\n * Hook cells, joined on call position.\n *\n * On position rather than on name because position is what a hook *is* to React\n * — the rule the linter enforces — and because a component that ran a different\n * number of hooks took a different branch before it rendered anything. That case\n * arrives here as cells present on one side only, which is a reading rather than\n * a join failure, and one of the loudest available.\n */\nfunction compareCells(\n left: readonly HeldCell[],\n right: readonly HeldCell[],\n into: MovedInput[],\n): void {\n const before = new Map(left.map((cell) => [cell.index, cell]));\n const after = new Map(right.map((cell) => [cell.index, cell]));\n\n for (const index of [...new Set([...before.keys(), ...after.keys()])].sort((a, b) => a - b)) {\n const from = before.get(index);\n const to = after.get(index);\n if (from?.digest === to?.digest && from?.hook === to?.hook) continue;\n into.push({\n kind: 'hook',\n name: to?.hook ?? from?.hook ?? '(unknown)',\n index,\n from: from?.digest,\n to: to?.digest,\n });\n }\n}\n"]}
@@ -0,0 +1,56 @@
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
+ export type PropertyImpact =
26
+ /** Reflows: box sizes and positions may change, here and elsewhere. */
27
+ 'layout'
28
+ /** Repaints in place. The box tree is untouched, so nothing moves. */
29
+ | 'paint'
30
+ /** Compositing only: transform, opacity, stacking. No reflow, no repaint. */
31
+ | 'composite';
32
+ /**
33
+ * Impact of a property change.
34
+ *
35
+ * An unrecognized property is reported as `layout`, the widest answer. Impact is
36
+ * used to *rule out* collateral, so an unknown treated as paint-only would let a
37
+ * real reflow pass unexamined — the over-reporting direction, as everywhere else.
38
+ */
39
+ export declare function impactOf(property: string): PropertyImpact;
40
+ /**
41
+ * Whether a change to this property can move anything.
42
+ *
43
+ * The question the cheap tier can answer without a layout engine, and the reason
44
+ * this axis exists.
45
+ */
46
+ export declare function canReflow(property: string): boolean;
47
+ export type AggregateImpact = PropertyImpact | 'mixed' | 'structural';
48
+ /**
49
+ * Combine impacts for a group of changes.
50
+ *
51
+ * `structural` wins over everything: a node appearing or disappearing is not a
52
+ * property change at all, and describing it in terms of repaint versus reflow
53
+ * would understate it.
54
+ */
55
+ export declare function aggregateImpact(impacts: readonly (PropertyImpact | 'structural')[]): AggregateImpact;
56
+ //# sourceMappingURL=impact.d.ts.map