@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,328 @@
1
+ /**
2
+ * Where one component's nodes stop and the next component's begin.
3
+ *
4
+ * Two relations run upward out of a rendered node and they are not the same
5
+ * relation. **Parent** is what is above it — React's `return` chain, the boxes
6
+ * it ended up inside. **Owner** is what rendered it — React's `_debugOwner`, the
7
+ * code that wrote the element. A layout wrapper is the parent of everything
8
+ * handed to it and the owner of none of it.
9
+ *
10
+ * Both are needed here, for different halves of the same answer:
11
+ *
12
+ * - **Parent decides nesting.** A boundary owns a contiguous region of the
13
+ * document, and what encloses that region is a fact about the tree. Reading
14
+ * the owner for this would produce a boundary set that is not a partition.
15
+ * - **Owner decides membership and naming.** A component's own content is what
16
+ * *it* wrote. Content it was handed is a hole in its output — present, sized,
17
+ * positioned by it, and authored somewhere else.
18
+ *
19
+ * Reading the parent for both — which is what a walk over `owners[0]` does — has
20
+ * two consequences, and they are the two shapes a design system is made of:
21
+ *
22
+ * - **A component that renders only components disappears.** It authors no host
23
+ * node, so it is never any node's nearest enclosing composite, so it is a
24
+ * boundary nowhere. That is every variant wrapper (`DangerButton` returning a
25
+ * `Button`) and every page-level assembly. The information is not missing —
26
+ * the composite is in the chain, one rung up — it is discarded by reading only
27
+ * the head of it. {@link boundaries} enters every rung the chain crosses.
28
+ * - **A container absorbs its caller's content.** `Card` renders one `div` and
29
+ * whatever it was given; hashed by enclosure, its digest moves whenever a
30
+ * caller passes something else, while its props digest — which excludes
31
+ * `children` — says its inputs held. {@link shapeOf} names a child boundary
32
+ * only when this component placed it, and leaves an anonymous hole otherwise.
33
+ *
34
+ * The owner half is a development-build artefact: React populates `_debugOwner`
35
+ * from `element._owner`, and a production bundle does not. Absent, both rules
36
+ * degrade to naming everything, which is the enclosure answer — coarser, never
37
+ * wrong in a new direction.
38
+ *
39
+ * Nothing here is exported from the package. The callers are siblings.
40
+ */
41
+ /** A node whose provenance chain broke. Not a filler category; see `RootKind`. */
42
+ export const UNATTRIBUTED = '(unattributed)';
43
+ /**
44
+ * Properties whose *computed* value is layout output rather than authored input.
45
+ *
46
+ * The distinction this list exists for is not stylistic. Under a profile with a
47
+ * layout engine the snapshot carries the engine's resolved values, so a block
48
+ * element's computed `height` is whatever its contents made it — and a button
49
+ * two levels down growing by six pixels moves the computed height of every
50
+ * ancestor. Hashed as *style*, that reports every enclosing component as having
51
+ * changed, which is precisely the "area ranks the displaced above the displacer"
52
+ * failure the cause hashes exist to fix, arriving through a different door.
53
+ *
54
+ * Measured on `cases/storybook-case`: one padding edit inside `Button` made
55
+ * `Tokens`, `Stack`, `Card` and the unattributed root all report a moved style
56
+ * hash, so *every* component in every affected story was named a cause.
57
+ *
58
+ * They are folded into `geometry` instead, where "this component's box is a
59
+ * different size" already belongs and where it correctly does not make a cause.
60
+ * Under a profile *without* layout there are no computed values, so these are
61
+ * authored declarations like any other and stay in `style` — that tier has no
62
+ * `geometry` digest to move them to, and a declared `width: 100px` really is the
63
+ * component's own content.
64
+ */
65
+ export const LAYOUT_OUTPUT = new Set([
66
+ 'width',
67
+ 'height',
68
+ 'min-width',
69
+ 'min-height',
70
+ 'max-width',
71
+ 'max-height',
72
+ // The one nobody guesses, and the one that was actually doing the damage.
73
+ // `transform-origin` computes to half the border box — `512px 41px` — so it
74
+ // moves whenever the box does, on every element, including ones that declare
75
+ // no transform at all. Removing `width` and `height` changed nothing on the
76
+ // case measurement; removing this is what made a padding edit inside `Button`
77
+ // stop naming `Tokens`, `Stack`, `Card` and the root as causes.
78
+ 'transform-origin',
79
+ // Chromium returns *used* track sizes here — `"70.39px 953.61px"` — never the
80
+ // author's `auto 1fr`, so nothing declared survives to be lost. That is what
81
+ // makes these two safe on a list, and it is exactly what is not true of the
82
+ // properties named below.
83
+ 'grid-template-columns',
84
+ 'grid-template-rows',
85
+ ]);
86
+ function ownershipOf(node) {
87
+ const owners = node.provenance?.owners;
88
+ if (owners === undefined || owners.length === 0) {
89
+ return { stack: [UNATTRIBUTED], placedBy: [undefined], props: [undefined] };
90
+ }
91
+ const stack = [];
92
+ const placedBy = [];
93
+ const props = [];
94
+ for (let index = owners.length - 1; index >= 0; index -= 1) {
95
+ const frame = owners[index];
96
+ stack.push(frame.name);
97
+ placedBy.push(frame.createdBy);
98
+ props.push(frame.propsDigest);
99
+ }
100
+ // FIXME: what should this name when the author is a slot?
101
+ //
102
+ // Radix `Slot` under `asChild` merges the caller's className onto the child's
103
+ // own host node, so `<Button asChild><Link/></Button>` renders one `<a>` whose
104
+ // author is next/link's `LinkComponent` and whose every styling decision came
105
+ // from `Button`. The owner rule then reports a name out of `node_modules` for a
106
+ // region an edit to `button.tsx` moved, and both halves are individually
107
+ // correct: `Button` really did author no host node, and `LinkComponent` really
108
+ // did write the one that moved.
109
+ //
110
+ // Open, because the naive fix is worse. Preferring the nearest caller whenever
111
+ // the author is unfamiliar would credit every container with its children's
112
+ // changes, which is exactly the enclosure answer the owner rule exists to
113
+ // refuse. Whether a slot is distinguishable from an ordinary hand-off at this
114
+ // rung has not been read — `Slot` is a component like any other from here.
115
+ const author = node.provenance?.createdBy;
116
+ if (author !== undefined && author !== stack.at(-1)) {
117
+ // A slotted region received no props of its own: it is markup, handed over
118
+ // whole. Absent rather than the container's digest, which would claim the
119
+ // author was called with inputs it never saw.
120
+ stack.push(author);
121
+ placedBy.push(author);
122
+ props.push(undefined);
123
+ }
124
+ return { stack, placedBy, props };
125
+ }
126
+ /** How many rungs two stacks agree on, from the outside in. */
127
+ function sharedRungs(a, b) {
128
+ const limit = Math.min(a.length, b.length);
129
+ let shared = 0;
130
+ while (shared < limit && a[shared] === b[shared])
131
+ shared += 1;
132
+ return shared;
133
+ }
134
+ /** Whether a node's own content belongs to this boundary rather than one below. */
135
+ export function holds(boundary, node) {
136
+ const { stack } = ownershipOf(node);
137
+ return stack.length === boundary.rung + 1 && stack[boundary.rung] === boundary.component;
138
+ }
139
+ /**
140
+ * Every boundary root in the subject, in document order.
141
+ *
142
+ * A boundary opens at each rung a node's ownership stack adds to its parent's,
143
+ * plus the subject root. Several can open at one node — a page assembly, the
144
+ * card it returns and the stack inside that card all begin at the same element —
145
+ * which is what makes a component that renders only components visible.
146
+ *
147
+ * Collected in a separate pre-order pass rather than discovered during hashing,
148
+ * so that instance order is document order exactly. Discovering them while
149
+ * walking would order them by boundary depth instead, and "the second instance"
150
+ * would mean something different in a nested tree.
151
+ */
152
+ export function boundaries(root) {
153
+ const found = [];
154
+ const visit = (node, parent, enclosing, depth) => {
155
+ const { stack, placedBy, props } = ownershipOf(node);
156
+ const opened = parent === null ? 0 : sharedRungs(parent, stack);
157
+ let within = enclosing;
158
+ let below = depth;
159
+ for (let rung = opened; rung < stack.length; rung += 1) {
160
+ const digest = props[rung];
161
+ const placer = placedBy[rung];
162
+ found.push({
163
+ node,
164
+ component: stack[rung],
165
+ ...(within === null ? {} : { within }),
166
+ depth: below,
167
+ rung,
168
+ ...(digest === undefined ? {} : { props: digest }),
169
+ ...(placer === undefined ? {} : { placedBy: placer }),
170
+ });
171
+ within = stack[rung];
172
+ below += 1;
173
+ }
174
+ for (const child of node.children)
175
+ visit(child, stack, within, below);
176
+ };
177
+ visit(root, null, null, 0);
178
+ return found;
179
+ }
180
+ /**
181
+ * One boundary's content, stopping at nested boundaries.
182
+ *
183
+ * Where a child belongs to another component, this records a placeholder rather
184
+ * than descending. That is the whole design: a component's hash moves when *its
185
+ * own* code changes, and what a child renders internally is not that. Hashing
186
+ * whole subtrees instead would move every ancestor on any leaf edit, and the
187
+ * page root would change on every commit.
188
+ *
189
+ * **The placeholder names the child only when this component placed it.** A
190
+ * component that wrote `<Stack>` owns that choice, and swapping it for a `<Card>`
191
+ * is its own change; a container that was handed a `<Stack>` chose nothing, and
192
+ * naming it would make the container's digest a function of its callers. So a
193
+ * slot is anonymous, and `Card` renders the same bytes on every page that uses
194
+ * it, whatever it was given. The cost is stated in ADR-0035: the *number* of
195
+ * slotted children still reaches the digest, so a caller passing three where it
196
+ * passed two moves the container.
197
+ *
198
+ * Falls back to naming every child where `_debugOwner` is absent, which is the
199
+ * enclosure answer this replaced.
200
+ *
201
+ * Paths are not hashed. A path is an address that shifts when an unrelated
202
+ * sibling is inserted, so hashing one reports a change nobody made.
203
+ */
204
+ export function shapeOf(boundary, layout, rename) {
205
+ const { node, component, rung } = boundary;
206
+ const style = [];
207
+ const geometry = [];
208
+ const semantics = [];
209
+ const text = [];
210
+ const wiring = [];
211
+ const renders = [];
212
+ const tokens = new Set();
213
+ let nodes = 0;
214
+ const walk = (current) => {
215
+ nodes += 1;
216
+ for (const token of Object.keys(current.tokens ?? {}))
217
+ tokens.add(token);
218
+ const declared = {};
219
+ const measured = {};
220
+ for (const [property, value] of Object.entries(current.style)) {
221
+ const resolved = rename === undefined ? value : rename.style(value);
222
+ if (layout && LAYOUT_OUTPUT.has(property))
223
+ measured[property] = resolved;
224
+ else
225
+ declared[property] = resolved;
226
+ }
227
+ style.push({ style: declared, tokens: renamedTokens(current, rename) });
228
+ // An entry per node, always. `null` for a node with no rect keeps position in
229
+ // the list meaningful: dropping the entry would let two different trees agree
230
+ // by coincidence.
231
+ geometry.push({
232
+ rect: current.rect ? { ...current.rect } : null,
233
+ // Beside the rect, not instead of it. A computed `max-height` that changed
234
+ // while the box did not is still a fact about this element's geometry, and
235
+ // dropping it would make the two digests disagree about what a box is.
236
+ ...(Object.keys(measured).length > 0 ? { measured } : {}),
237
+ });
238
+ // One entry per node in each list, always — `null` rather than omitted, for
239
+ // the reason the geometry list gives: dropping an entry lets two different
240
+ // trees agree by coincidence, and here it would let a heading losing its
241
+ // name look like a heading that never had one.
242
+ semantics.push({
243
+ role: current.role ?? null,
244
+ name: current.name ?? null,
245
+ state: (current.state ?? null),
246
+ });
247
+ text.push(current.text ?? null);
248
+ // `null` for a node that reports none, for the reason every list here gives:
249
+ // an omitted entry lets two different trees agree by coincidence. Most nodes
250
+ // report `null` — wiring attaches to a component's root node and not to the
251
+ // elements beneath it (see `wiringOf`), so a boundary's band is a short
252
+ // signal in a long run of nulls, and the nulls are what make its position
253
+ // mean anything.
254
+ wiring.push((current.wiring ?? null));
255
+ return {
256
+ tag: current.tag,
257
+ alias: renamedAlias(current, rename),
258
+ portalled: current.portalled,
259
+ attributes: renamedAttributes(current, rename),
260
+ children: current.children.map((child) => entry(child)),
261
+ };
262
+ };
263
+ /**
264
+ * Every node inside one boundary carries the same stack, by construction.
265
+ *
266
+ * So the comparison a child is measured against is fixed for the whole walk,
267
+ * and a zero-node boundary — a component that renders only components, whose
268
+ * root node already belongs to the boundary below it — measures against the
269
+ * same thing.
270
+ */
271
+ const home = ownershipOf(node).stack.slice(0, rung + 1);
272
+ /**
273
+ * A node's contribution: its own content, or a stand-in for what is beneath.
274
+ *
275
+ * The rung a child leaves at is where its stack stops agreeing with `home` —
276
+ * normally the next rung down, and shallower wherever a subject root carries
277
+ * no provenance and the application starts again inside it.
278
+ */
279
+ const entry = (child) => {
280
+ if (holds(boundary, child))
281
+ return walk(child);
282
+ const own = ownershipOf(child);
283
+ const left = sharedRungs(home, own.stack);
284
+ const beneath = own.stack[left] ?? own.stack.at(-1) ?? UNATTRIBUTED;
285
+ // Kept whichever way the digest goes: the edge is a true fact about the
286
+ // document, and the graph wants it even where the hash must not carry it.
287
+ renders.push(beneath);
288
+ const placer = own.placedBy[left];
289
+ return placer === undefined || placer === component ? { boundary: beneath } : { slot: null };
290
+ };
291
+ const structure = entry(node);
292
+ return {
293
+ structure,
294
+ semantics,
295
+ text,
296
+ style,
297
+ geometry,
298
+ wiring,
299
+ box: node.rect ?? null,
300
+ renders,
301
+ nodes,
302
+ tokens: [...tokens].sort(),
303
+ };
304
+ }
305
+ function renamedAlias(node, rename) {
306
+ if (rename === undefined || node.alias === undefined)
307
+ return node.alias;
308
+ return rename.alias(node.alias);
309
+ }
310
+ function renamedAttributes(node, rename) {
311
+ if (rename === undefined)
312
+ return node.attributes;
313
+ const renamed = {};
314
+ for (const [name, value] of Object.entries(node.attributes)) {
315
+ renamed[name] = rename.attribute(name, value);
316
+ }
317
+ return renamed;
318
+ }
319
+ function renamedTokens(node, rename) {
320
+ if (rename === undefined || node.tokens === undefined)
321
+ return node.tokens;
322
+ const renamed = {};
323
+ for (const [name, value] of Object.entries(node.tokens)) {
324
+ renamed[name] = rename.style(value);
325
+ }
326
+ return renamed;
327
+ }
328
+ //# sourceMappingURL=boundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boundary.js","sourceRoot":"","sources":["../../src/attribute/boundary.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CanonicalValue } from '../format/canonical.js';\nimport type { Rect } from '../format/capture.js';\nimport type { Digest } from '../format/hash.js';\nimport type { SemanticNode } from '../format/snapshot.js';\n\n/**\n * Where one component's nodes stop and the next component's begin.\n *\n * Two relations run upward out of a rendered node and they are not the same\n * relation. **Parent** is what is above it — React's `return` chain, the boxes\n * it ended up inside. **Owner** is what rendered it — React's `_debugOwner`, the\n * code that wrote the element. A layout wrapper is the parent of everything\n * handed to it and the owner of none of it.\n *\n * Both are needed here, for different halves of the same answer:\n *\n * - **Parent decides nesting.** A boundary owns a contiguous region of the\n * document, and what encloses that region is a fact about the tree. Reading\n * the owner for this would produce a boundary set that is not a partition.\n * - **Owner decides membership and naming.** A component's own content is what\n * *it* wrote. Content it was handed is a hole in its output — present, sized,\n * positioned by it, and authored somewhere else.\n *\n * Reading the parent for both — which is what a walk over `owners[0]` does — has\n * two consequences, and they are the two shapes a design system is made of:\n *\n * - **A component that renders only components disappears.** It authors no host\n * node, so it is never any node's nearest enclosing composite, so it is a\n * boundary nowhere. That is every variant wrapper (`DangerButton` returning a\n * `Button`) and every page-level assembly. The information is not missing —\n * the composite is in the chain, one rung up — it is discarded by reading only\n * the head of it. {@link boundaries} enters every rung the chain crosses.\n * - **A container absorbs its caller's content.** `Card` renders one `div` and\n * whatever it was given; hashed by enclosure, its digest moves whenever a\n * caller passes something else, while its props digest — which excludes\n * `children` — says its inputs held. {@link shapeOf} names a child boundary\n * only when this component placed it, and leaves an anonymous hole otherwise.\n *\n * The owner half is a development-build artefact: React populates `_debugOwner`\n * from `element._owner`, and a production bundle does not. Absent, both rules\n * degrade to naming everything, which is the enclosure answer — coarser, never\n * wrong in a new direction.\n *\n * Nothing here is exported from the package. The callers are siblings.\n */\n\n/** A node whose provenance chain broke. Not a filler category; see `RootKind`. */\nexport const UNATTRIBUTED = '(unattributed)';\n\n/**\n * Properties whose *computed* value is layout output rather than authored input.\n *\n * The distinction this list exists for is not stylistic. Under a profile with a\n * layout engine the snapshot carries the engine's resolved values, so a block\n * element's computed `height` is whatever its contents made it — and a button\n * two levels down growing by six pixels moves the computed height of every\n * ancestor. Hashed as *style*, that reports every enclosing component as having\n * changed, which is precisely the \"area ranks the displaced above the displacer\"\n * failure the cause hashes exist to fix, arriving through a different door.\n *\n * Measured on `cases/storybook-case`: one padding edit inside `Button` made\n * `Tokens`, `Stack`, `Card` and the unattributed root all report a moved style\n * hash, so *every* component in every affected story was named a cause.\n *\n * They are folded into `geometry` instead, where \"this component's box is a\n * different size\" already belongs and where it correctly does not make a cause.\n * Under a profile *without* layout there are no computed values, so these are\n * authored declarations like any other and stay in `style` — that tier has no\n * `geometry` digest to move them to, and a declared `width: 100px` really is the\n * component's own content.\n */\nexport const LAYOUT_OUTPUT: ReadonlySet<string> = new Set([\n 'width',\n 'height',\n 'min-width',\n 'min-height',\n 'max-width',\n 'max-height',\n // The one nobody guesses, and the one that was actually doing the damage.\n // `transform-origin` computes to half the border box — `512px 41px` — so it\n // moves whenever the box does, on every element, including ones that declare\n // no transform at all. Removing `width` and `height` changed nothing on the\n // case measurement; removing this is what made a padding edit inside `Button`\n // stop naming `Tokens`, `Stack`, `Card` and the root as causes.\n 'transform-origin',\n // Chromium returns *used* track sizes here — `\"70.39px 953.61px\"` — never the\n // author's `auto 1fr`, so nothing declared survives to be lost. That is what\n // makes these two safe on a list, and it is exactly what is not true of the\n // properties named below.\n 'grid-template-columns',\n 'grid-template-rows',\n]);\n\n/**\n * Five more shapes leak, and a longer list is the wrong fix. Measured.\n *\n * Editing only a descendant's padding in a real Chromium moves the computed\n * value of an ancestor's `padding-*` (percentage padding in a shrink-to-fit\n * box), `margin-*` (an `auto` margin centring a `fit-content` block),\n * `top`/`right`/`bottom`/`left` (an absolutely positioned element anchored to an\n * edge that moved) and `transform` (a `translate(-50%,-50%)` matrix, the exact\n * sibling of the `transform-origin` case above). Each one makes an enclosing\n * component a false cause.\n *\n * Adding them here was tried and is worse than the disease: with `padding-*` on\n * the list, `causes.test.ts`'s headline case — a padding edit *inside* `Button`\n * — stops naming `Button` at all. For these properties the computed value **is**\n * the authored value in the ordinary case, and a property name cannot tell the\n * two apart. Only a *value* can, and the only place that knows is `resolveStyle`\n * in `rules/normalize/cascade.ts`, which sees both the cascade's winner and the\n * engine's override and currently keeps no record of which superseded which.\n *\n * So the limit is stated rather than half-fixed: a component whose own\n * declarations use percentage padding, auto margins, edge-anchored absolute\n * positioning or a percentage translate can be named a cause by a change that\n * was not its own. The failure is a *false* cause, never a missed one, which is\n * the direction that costs a reader attention rather than a regression.\n */\n\n/**\n * Every component holding a node, outermost first, and who placed each one.\n *\n * The whole chain, not its head. `TodoFooter` rendering nothing but a `Stack`\n * appears here at the rung below `Stack`, which is the only place it appears at\n * all — it authors no element, so no node has it as a nearest enclosing\n * composite, and a walk that reads `owners[0]` cannot see it.\n *\n * `placedBy[i]` is who wrote the element for `stack[i]`: `OwnerFrame.createdBy`,\n * which is the component that decided a `<Stack>` belongs at this point, as\n * distinct from `Stack` itself. Absent per rung on a production build.\n *\n * A host node whose author is not the innermost component is content that was\n * handed in: it sits inside the container's box and belongs to whoever wrote it,\n * so the author is pushed as a further rung. That keeps the boundary set a\n * partition of the document — the slotted region is nested inside the container\n * rather than lifted out of it — while attributing it to the code responsible.\n */\ninterface Ownership {\n readonly stack: readonly string[];\n readonly placedBy: readonly (string | undefined)[];\n readonly props: readonly (Digest | undefined)[];\n}\n\nfunction ownershipOf(node: SemanticNode): Ownership {\n const owners = node.provenance?.owners;\n if (owners === undefined || owners.length === 0) {\n return { stack: [UNATTRIBUTED], placedBy: [undefined], props: [undefined] };\n }\n\n const stack: string[] = [];\n const placedBy: (string | undefined)[] = [];\n const props: (Digest | undefined)[] = [];\n for (let index = owners.length - 1; index >= 0; index -= 1) {\n const frame = owners[index]!;\n stack.push(frame.name);\n placedBy.push(frame.createdBy);\n props.push(frame.propsDigest);\n }\n\n // FIXME: what should this name when the author is a slot?\n //\n // Radix `Slot` under `asChild` merges the caller's className onto the child's\n // own host node, so `<Button asChild><Link/></Button>` renders one `<a>` whose\n // author is next/link's `LinkComponent` and whose every styling decision came\n // from `Button`. The owner rule then reports a name out of `node_modules` for a\n // region an edit to `button.tsx` moved, and both halves are individually\n // correct: `Button` really did author no host node, and `LinkComponent` really\n // did write the one that moved.\n //\n // Open, because the naive fix is worse. Preferring the nearest caller whenever\n // the author is unfamiliar would credit every container with its children's\n // changes, which is exactly the enclosure answer the owner rule exists to\n // refuse. Whether a slot is distinguishable from an ordinary hand-off at this\n // rung has not been read — `Slot` is a component like any other from here.\n const author = node.provenance?.createdBy;\n if (author !== undefined && author !== stack.at(-1)) {\n // A slotted region received no props of its own: it is markup, handed over\n // whole. Absent rather than the container's digest, which would claim the\n // author was called with inputs it never saw.\n stack.push(author);\n placedBy.push(author);\n props.push(undefined);\n }\n\n return { stack, placedBy, props };\n}\n\n/** How many rungs two stacks agree on, from the outside in. */\nfunction sharedRungs(a: readonly string[], b: readonly string[]): number {\n const limit = Math.min(a.length, b.length);\n let shared = 0;\n while (shared < limit && a[shared] === b[shared]) shared += 1;\n return shared;\n}\n\nexport interface Boundary {\n readonly node: SemanticNode;\n readonly component: string;\n /** The enclosing boundary's component, absent on the subject root. */\n readonly within?: string;\n /** Boundaries between this one and the subject root. The root is `0`. */\n readonly depth: number;\n /**\n * This component's rung in the node's ownership stack.\n *\n * Distinct from `depth`, which counts enclosing boundaries. They diverge\n * wherever a subject root has no provenance: the `(unattributed)` container is\n * a boundary at depth 0 holding a stack of one, and the application inside it\n * starts again at rung 0 while sitting at depth 1.\n */\n readonly rung: number;\n /**\n * Digest of the props this boundary received, from its own `OwnerFrame`.\n *\n * Read at this rung rather than from `owners[0]`, which is the innermost\n * component and belongs to whichever boundary is deepest at this node — the\n * distinction only exists because several boundaries can share a root.\n */\n readonly props?: Digest;\n /** The component whose JSX placed this one. Absent on a production build. */\n readonly placedBy?: string;\n}\n\n/** Whether a node's own content belongs to this boundary rather than one below. */\nexport function holds(boundary: Boundary, node: SemanticNode): boolean {\n const { stack } = ownershipOf(node);\n return stack.length === boundary.rung + 1 && stack[boundary.rung] === boundary.component;\n}\n\n/**\n * Every boundary root in the subject, in document order.\n *\n * A boundary opens at each rung a node's ownership stack adds to its parent's,\n * plus the subject root. Several can open at one node — a page assembly, the\n * card it returns and the stack inside that card all begin at the same element —\n * which is what makes a component that renders only components visible.\n *\n * Collected in a separate pre-order pass rather than discovered during hashing,\n * so that instance order is document order exactly. Discovering them while\n * walking would order them by boundary depth instead, and \"the second instance\"\n * would mean something different in a nested tree.\n */\nexport function boundaries(root: SemanticNode): readonly Boundary[] {\n const found: Boundary[] = [];\n\n const visit = (\n node: SemanticNode,\n parent: readonly string[] | null,\n enclosing: string | null,\n depth: number,\n ): void => {\n const { stack, placedBy, props } = ownershipOf(node);\n const opened = parent === null ? 0 : sharedRungs(parent, stack);\n\n let within = enclosing;\n let below = depth;\n for (let rung = opened; rung < stack.length; rung += 1) {\n const digest = props[rung];\n const placer = placedBy[rung];\n found.push({\n node,\n component: stack[rung]!,\n ...(within === null ? {} : { within }),\n depth: below,\n rung,\n ...(digest === undefined ? {} : { props: digest }),\n ...(placer === undefined ? {} : { placedBy: placer }),\n });\n within = stack[rung]!;\n below += 1;\n }\n\n for (const child of node.children) visit(child, stack, within, below);\n };\n\n visit(root, null, null, 0);\n return found;\n}\n\nexport interface Shape {\n readonly structure: CanonicalValue;\n readonly semantics: CanonicalValue;\n readonly text: CanonicalValue;\n readonly style: CanonicalValue;\n readonly geometry: CanonicalValue;\n\n /**\n * How the framework holds this boundary, per node that reports it.\n *\n * Its own band because it answers its own question. `structure` through\n * `geometry` all read what the renderer produced; this reads what the\n * component *is* — its hook shape, its wrappers, the contexts it subscribes\n * to, the keys it is reconciled under. Two components can agree on all five\n * content bands and disagree here, and when they do, they behave differently\n * under every change that follows.\n */\n readonly wiring: CanonicalValue;\n\n /**\n * The box this boundary's root node occupies. Not part of any digest.\n *\n * Beside the digests rather than inside `geometry`, which is the whole subtree:\n * this is the only thing on the record that can say a control grew eight pixels\n * rather than merely stopped being what it was. `null` where nothing was laid out.\n */\n readonly box: Rect | null;\n\n /** Child boundaries encountered, in document order. Not part of any digest. */\n readonly renders: readonly string[];\n /** Nodes this boundary owns, counting its own root. Not part of any digest. */\n readonly nodes: number;\n /**\n * Custom properties this boundary's own nodes resolved through, sorted.\n *\n * Not part of any digest — the *values* are already inside `style`, and the\n * names are carried beside it so a token that moved can be joined to the\n * components that read it. Off the boundary rather than off the subject on\n * purpose: every subject on a themed page resolves through every token in the\n * theme, so a subject-level list names them all and explains nothing.\n */\n readonly tokens: readonly string[];\n}\n\n/**\n * How a value that may carry a structural alias is rewritten before hashing.\n *\n * `undefined` for the per-name hashes, which have always hashed the alias the\n * normalizer assigned and must keep doing so byte for byte. Supplied by the\n * per-instance hashes, which need a boundary-local alias space — see\n * {@link ./instances.js}, where the argument for it lives.\n */\nexport interface Rename {\n readonly attribute: (name: string, value: string) => string;\n readonly style: (value: string) => string;\n readonly alias: (alias: string) => string;\n}\n\n/**\n * One boundary's content, stopping at nested boundaries.\n *\n * Where a child belongs to another component, this records a placeholder rather\n * than descending. That is the whole design: a component's hash moves when *its\n * own* code changes, and what a child renders internally is not that. Hashing\n * whole subtrees instead would move every ancestor on any leaf edit, and the\n * page root would change on every commit.\n *\n * **The placeholder names the child only when this component placed it.** A\n * component that wrote `<Stack>` owns that choice, and swapping it for a `<Card>`\n * is its own change; a container that was handed a `<Stack>` chose nothing, and\n * naming it would make the container's digest a function of its callers. So a\n * slot is anonymous, and `Card` renders the same bytes on every page that uses\n * it, whatever it was given. The cost is stated in ADR-0035: the *number* of\n * slotted children still reaches the digest, so a caller passing three where it\n * passed two moves the container.\n *\n * Falls back to naming every child where `_debugOwner` is absent, which is the\n * enclosure answer this replaced.\n *\n * Paths are not hashed. A path is an address that shifts when an unrelated\n * sibling is inserted, so hashing one reports a change nobody made.\n */\nexport function shapeOf(boundary: Boundary, layout: boolean, rename?: Rename): Shape {\n const { node, component, rung } = boundary;\n const style: CanonicalValue[] = [];\n const geometry: CanonicalValue[] = [];\n const semantics: CanonicalValue[] = [];\n const text: CanonicalValue[] = [];\n const wiring: CanonicalValue[] = [];\n const renders: string[] = [];\n const tokens = new Set<string>();\n let nodes = 0;\n\n const walk = (current: SemanticNode): CanonicalValue => {\n nodes += 1;\n for (const token of Object.keys(current.tokens ?? {})) tokens.add(token);\n const declared: Record<string, string> = {};\n const measured: Record<string, string> = {};\n\n for (const [property, value] of Object.entries(current.style)) {\n const resolved = rename === undefined ? value : rename.style(value);\n if (layout && LAYOUT_OUTPUT.has(property)) measured[property] = resolved;\n else declared[property] = resolved;\n }\n\n style.push({ style: declared, tokens: renamedTokens(current, rename) });\n // An entry per node, always. `null` for a node with no rect keeps position in\n // the list meaningful: dropping the entry would let two different trees agree\n // by coincidence.\n geometry.push({\n rect: current.rect ? { ...current.rect } : null,\n // Beside the rect, not instead of it. A computed `max-height` that changed\n // while the box did not is still a fact about this element's geometry, and\n // dropping it would make the two digests disagree about what a box is.\n ...(Object.keys(measured).length > 0 ? { measured } : {}),\n });\n\n // One entry per node in each list, always — `null` rather than omitted, for\n // the reason the geometry list gives: dropping an entry lets two different\n // trees agree by coincidence, and here it would let a heading losing its\n // name look like a heading that never had one.\n semantics.push({\n role: current.role ?? null,\n name: current.name ?? null,\n state: (current.state ?? null) as CanonicalValue,\n });\n text.push(current.text ?? null);\n // `null` for a node that reports none, for the reason every list here gives:\n // an omitted entry lets two different trees agree by coincidence. Most nodes\n // report `null` — wiring attaches to a component's root node and not to the\n // elements beneath it (see `wiringOf`), so a boundary's band is a short\n // signal in a long run of nulls, and the nulls are what make its position\n // mean anything.\n wiring.push((current.wiring ?? null) as CanonicalValue);\n\n return {\n tag: current.tag,\n alias: renamedAlias(current, rename),\n portalled: current.portalled,\n attributes: renamedAttributes(current, rename),\n children: current.children.map((child) => entry(child)),\n };\n };\n\n /**\n * Every node inside one boundary carries the same stack, by construction.\n *\n * So the comparison a child is measured against is fixed for the whole walk,\n * and a zero-node boundary — a component that renders only components, whose\n * root node already belongs to the boundary below it — measures against the\n * same thing.\n */\n const home = ownershipOf(node).stack.slice(0, rung + 1);\n\n /**\n * A node's contribution: its own content, or a stand-in for what is beneath.\n *\n * The rung a child leaves at is where its stack stops agreeing with `home` —\n * normally the next rung down, and shallower wherever a subject root carries\n * no provenance and the application starts again inside it.\n */\n const entry = (child: SemanticNode): CanonicalValue => {\n if (holds(boundary, child)) return walk(child);\n\n const own = ownershipOf(child);\n const left = sharedRungs(home, own.stack);\n const beneath = own.stack[left] ?? own.stack.at(-1) ?? UNATTRIBUTED;\n // Kept whichever way the digest goes: the edge is a true fact about the\n // document, and the graph wants it even where the hash must not carry it.\n renders.push(beneath);\n\n const placer = own.placedBy[left];\n return placer === undefined || placer === component ? { boundary: beneath } : { slot: null };\n };\n\n const structure = entry(node);\n return {\n structure,\n semantics,\n text,\n style,\n geometry,\n wiring,\n box: node.rect ?? null,\n renders,\n nodes,\n tokens: [...tokens].sort(),\n };\n}\n\nfunction renamedAlias(node: SemanticNode, rename?: Rename): string | undefined {\n if (rename === undefined || node.alias === undefined) return node.alias;\n return rename.alias(node.alias);\n}\n\nfunction renamedAttributes(\n node: SemanticNode,\n rename?: Rename,\n): Readonly<Record<string, string>> {\n if (rename === undefined) return node.attributes;\n\n const renamed: Record<string, string> = {};\n for (const [name, value] of Object.entries(node.attributes)) {\n renamed[name] = rename.attribute(name, value);\n }\n return renamed;\n}\n\nfunction renamedTokens(\n node: SemanticNode,\n rename?: Rename,\n): Readonly<Record<string, string>> | undefined {\n if (rename === undefined || node.tokens === undefined) return node.tokens;\n\n const renamed: Record<string, string> = {};\n for (const [name, value] of Object.entries(node.tokens)) {\n renamed[name] = rename.style(value);\n }\n return renamed;\n}\n"]}
@@ -0,0 +1,96 @@
1
+ import type { Provenance, SourceLocation, StackFrame } from '../format/provenance.js';
2
+ import type { NodePath, SemanticSnapshot } from '../format/snapshot.js';
3
+ /**
4
+ * Spending the frames a page read, to get the file a reviewer opens.
5
+ *
6
+ * This is the half of the zero-install path that cannot happen in the page. A
7
+ * frame names the module the browser was *served*, and turning that into a line
8
+ * in the repository means fetching that module and reading the source map the
9
+ * build already emitted beside it. Fetching is not something a page agent should
10
+ * do — it runs inside somebody else's application, on a budget measured against
11
+ * their render loop — and it is exactly what the collector is for.
12
+ *
13
+ * **The economics are what make this worth doing rather than clever.** Measured
14
+ * on a 4211-node document: every fiber carried a stack, and between them they
15
+ * held **14 distinct call sites**. A hundred-row table writes two thousand cells
16
+ * from one line of JSX. So the work is per *call site*, not per node, and the
17
+ * cache below is not an optimization but the thing that makes the cost bounded —
18
+ * a handful of module fetches for a whole page.
19
+ *
20
+ * **And it is asked on a signal rather than on every capture.** Fourteen sites is
21
+ * a bounded cost, not a free one, and a run that settles every subject on its
22
+ * document digest has nobody to hand a location to: no region was drawn, no
23
+ * finding was raised, and the fetches would answer a question nothing asked. So
24
+ * frames ride the snapshot — they are provenance, and no hash projects
25
+ * provenance — and {@link locateSites} spends them for the handful of nodes a
26
+ * report is about to name. A page whose only change is one button resolves one
27
+ * call site, not fourteen; a page that did not change resolves none.
28
+ *
29
+ * `fetchModule` is injected because `core` may not assume a network (ADR-0013),
30
+ * and because the right way to fetch differs by caller: a browser-driving
31
+ * collector should fetch from the page's own context, where the origin, the
32
+ * cookies and the dev server's module graph are already correct.
33
+ */
34
+ /** Fetch a module's text, or answer that it cannot be had. Never throws. */
35
+ export type FetchModule = (url: string) => Promise<string | null>;
36
+ export interface CallSiteResolver {
37
+ /** The location that wrote an element, from the frames its fiber carried. */
38
+ locate(frames: readonly StackFrame[]): Promise<SourceLocation | null>;
39
+ /** How many modules were fetched, and how many call sites they answered. */
40
+ readonly stats: CallSiteStats;
41
+ }
42
+ export interface CallSiteStats {
43
+ /** Distinct modules fetched. The number that costs anything. */
44
+ readonly modules: number;
45
+ /** Distinct call sites asked about — the ratio to `modules` is the point. */
46
+ readonly sites: number;
47
+ /** Call sites that resolved to a file. */
48
+ readonly located: number;
49
+ }
50
+ export declare function createCallSiteResolver(fetchModule: FetchModule): CallSiteResolver;
51
+ /**
52
+ * Provenance with its frames spent: `source` filled in, `stack` gone.
53
+ *
54
+ * Returned unchanged when there is nothing to do — which is the common case once
55
+ * a project installs `jsx-source`, and the case a production build is always in.
56
+ * Never throws: a report that names a location for most of its nodes is the
57
+ * normal outcome, and one node's unreachable module must not take the run down.
58
+ */
59
+ export declare function locateProvenance(provenance: Provenance, resolver: CallSiteResolver): Promise<Provenance>;
60
+ /**
61
+ * Anything a report points at: it names a node, and may already know its line.
62
+ *
63
+ * An `AttributedRegion` and a `Finding` are both this, arrived at from opposite
64
+ * directions — one from a mask, one from an inspection — and both are the *few*.
65
+ * That is the whole reason this shape is worth having rather than two functions:
66
+ * what makes resolution affordable is being asked about a handful of nodes, and
67
+ * a handful is what a region list and a finding list are.
68
+ */
69
+ export interface LocatableSite {
70
+ readonly path?: NodePath;
71
+ readonly source?: SourceLocation;
72
+ }
73
+ /**
74
+ * The line for each site a report is about to name, and for nothing else.
75
+ *
76
+ * This is the demand side of the zero-install path. The page reads frames off
77
+ * every fiber because reading them is nearly free; turning one into a file is
78
+ * not, and this is the only place that spends it.
79
+ *
80
+ * Three ways a site costs nothing, all of them the common case:
81
+ *
82
+ * - **It has no `path`.** A region no box contains names no node.
83
+ * - **It already has a `source`.** React ≤18 and `@variance-authority/jsx-source`
84
+ * both record the location outright, so there is nothing to resolve. Only
85
+ * React 19, which throws its location away and captures an `Error` instead,
86
+ * reaches the map.
87
+ * - **Its node carried no frames.** A production build captures nothing.
88
+ *
89
+ * And a subject that settled on its document digest never calls this at all,
90
+ * which is the point: no region, no finding, no fetch.
91
+ *
92
+ * Sites are resolved concurrently. The resolver's own cache collapses them onto
93
+ * the modules they share, so two regions in one component cost one fetch.
94
+ */
95
+ export declare function locateSites<T extends LocatableSite>(sites: readonly T[], snapshot: SemanticSnapshot, resolver: CallSiteResolver): Promise<readonly T[]>;
96
+ //# sourceMappingURL=call-site.d.ts.map