@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,79 @@
1
+ # @variance-authority/core
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5c34e6d: Say which input moved, not just which tag did
8
+
9
+ `partingOf(baseline, candidate)` climbs from a set of deltas to the component
10
+ boundary that owns them and names the input that carried the decision:
11
+ `Cart chose differently — useState #0 moved`, with `Summary` reported as having
12
+ been handed a different `expanded` and hanging under that line as a
13
+ manifestation rather than as a second finding. `explainParting` renders that
14
+ as lines. Two runs of one page that differ in a `<p>` where the other has a
15
+ `<span>` previously produced a structural delta and nothing to say about it;
16
+ the tag is now the symptom and the hook cell is the report.
17
+
18
+ `holdingOf` from `@variance-authority/react` supplies the evidence — props,
19
+ context values and hook cells as digests, per boundary, including
20
+ `useSyncExternalStore` snapshots so a store that moved outside React is
21
+ distinguished from a component that decided differently on its own. Values are
22
+ never carried, only digests, and a holding reaches no hash: `renderHash`,
23
+ `structureHash`, `styleHash`, band-exact component digests and
24
+ `componentInstances` are all unmoved by it, the same bargain `styleProvenance`
25
+ makes. `collect` in `@variance-authority/dom` takes `holdingOf` as a
26
+ caller-supplied reader, opt in separately from `wiringOf`, and `capture` in
27
+ `@variance-authority/unit-test` now accepts `provenanceOf`, `wiringOf` and
28
+ `holdingOf` so a unit test can read one.
29
+
30
+ A boundary whose own input could not be read is reported as `unread` and never
31
+ as nondeterminism: `undetermined` is reserved for a component whose every input
32
+ was read and agreed. A wrapper that roots a component boundary no longer
33
+ collapses, because collapsing it discarded the holding — the cost is that a run
34
+ reading holdings keeps wrappers a run without them removes, which is why both
35
+ sides of a comparison must be read the same way.
36
+
37
+ A component that ran no hooks is read as having run none, rather than as
38
+ unreadable. React writes `_debugHookTypes = null` on every fiber and fills it on
39
+ the first hook call, so the property's absence is the only silence — and
40
+ collapsing the two reported the one shape most worth calling nondeterministic, a
41
+ component with no props and no hooks that renders differently twice, as
42
+ something nothing could be said about.
43
+
44
+ `PartedBoundary.moved` names the properties the owned deltas named — `color`,
45
+ `padding-top`, `width` — and `explainParting` spends them on the delta line.
46
+ That is the last joint of the chain the rungs climb: a hook cell moved, a prop
47
+ carried it down, and this is what the prop turned into on the page. A count and
48
+ a band stop one link short of what somebody chasing a visual regression is
49
+ trying to name.
50
+
51
+ `Parting.slice` answers the question asked before which input moved: is this
52
+ worth opening. Three facts are read independently — did the component tree
53
+ move, did any input move, did the output move — and the combinations collapse
54
+ to six sentences. `refactor` is the one that pays for the rest: a component
55
+ tree that moved while the page did not is the receipt a refactor never gets,
56
+ since a pixel differ can say the screenshots match and nothing about what was
57
+ rewritten underneath. `flake` is `settled`'s opposite number and is refused on
58
+ silence — a run that read no boundary reports `unread`, never `flake`.
59
+
60
+ `explainParting` leads with that line, and stops enumerating manifestations
61
+ past three: one input at a fork can put a boundary on every component beneath
62
+ it, and nine lines carrying one decision bury the one line worth reading.
63
+
64
+ The whole layer is documented in `docs/parting.md`: the six slices, the seven
65
+ rungs, what a holding carries and what it deliberately does not.
66
+
67
+ ### Patch Changes
68
+
69
+ - e8fee66: Refuse a value whose state is not in its own enumerable keys.
70
+
71
+ `shapeValue` documented a `Date` as refused and encoded it instead. `typeof`
72
+ answers `object`, `Object.entries` answers empty, and the canonical text came
73
+ out `{}` — so a snapshot holding a timestamp addressed to the digest of an empty
74
+ object, and a later run comparing a different timestamp reported unchanged. The
75
+ same hole swallowed `Map`, `Set`, `URL`, and `RegExp`.
76
+
77
+ An object with no own enumerable keys and a prototype other than `Object`'s is
78
+ now refused by name at its pointer. A genuinely empty object is still a value,
79
+ and an instance carrying its own fields still serializes them.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Machine Garden
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ <p align="center"><img src="./mark.svg" alt="Variance Authority mark" width="72"></p>
2
+
3
+ # @variance-authority/core
4
+
5
+ > The Variance Authority format, rules, comparison, attribution and verdicts. Pure data in, pure data out, no DOM and no I/O.
6
+
7
+ A **subject** is whatever is under test — a component, a page, or a plain
8
+ value. A **capture** is the raw material a collector records from it once,
9
+ before anything is compared. Collectors extract captures; this package
10
+ normalizes and adjudicates them, and never captures anything itself.
11
+
12
+ ```bash
13
+ npm install --save-dev @variance-authority/core
14
+ ```
15
+ ## Use this package when
16
+
17
+ Install `@variance-authority/core` when the input is already produced: a
18
+ capture, a semantic snapshot, a render document (serialized subject markup,
19
+ ready to be rasterized elsewhere), a raster mask (`ChangeMask`: a per-pixel
20
+ changed/unchanged bitmap, not an image), or a plain value. The package does
21
+ not collect a DOM, read PNG bytes, launch a renderer, or select a test
22
+ runner — for that, install `@variance-authority/dom` for a live DOM,
23
+ `@variance-authority/react` for React provenance (the chain of component
24
+ ownership attached to each node), and `@variance-authority/png` when the
25
+ input is a PNG.
26
+
27
+ ## Entrypoints
28
+
29
+ Seven groups. Five of them are the order an answer travels through; `core/plan`
30
+ and `core/relate` sit outside that line, because both are asked *before* anything
31
+ is captured — one decides which baselines the run can reach at all, the other
32
+ decides which subjects are worth reaching for. The default entrypoint is all
33
+ seven and is what most callers want.
34
+
35
+ | entrypoint | holds |
36
+ |---|---|
37
+ | `core/format` | what a subject *is*: capture, snapshot, document, identity, hashing |
38
+ | `core/rules` | the versioned opinions: allowlist, applicability, cascade, canonicalization |
39
+ | `core/compare` | two snapshots become deltas — and **no verdict** |
40
+ | `core/attribute` | a position becomes a component becomes a file |
41
+ | `core/judge` | policy: verdicts, intent claims, ignores, the docket a reader is handed |
42
+ | `core/plan` | the whole configuration of a run — profile, ruleset version, viewport, policy, interventions — as one value, plus the identity digest derived from it |
43
+ | `core/relate` | what rests on what: a file graph in adjacency form, the components a change reaches, and a closure digest over each one |
44
+
45
+ Three terms recur across those groups. A **profile** records what a collector
46
+ was capable of observing — jsdom sees structure and declared style, chromium
47
+ adds layout and pixels. An **identity** is the content hash that addresses a
48
+ result; two runs are comparable only when their identities match. A
49
+ **verdict** is one of six words this package will commit to: `unchanged`,
50
+ `inherited`, `authorized`, `needs-review`, `violation`, `unexplained`.
51
+
52
+ The groups exist for callers who genuinely want one. Somebody implementing the
53
+ capture format for a renderer this project has never met needs `core/format` and
54
+ would be misled by everything else. Somebody deciding where a baseline is stored,
55
+ or whether two runs may be compared at all, needs `core/plan` and nothing else:
56
+ the digest it derives is the address, so changing any part of the plan changes
57
+ which baselines the run can see.
58
+
59
+ ## Comparison and policy
60
+
61
+ `compare` says **what moved**. `judge` says **whether anyone should mind**.
62
+
63
+ Between them sits `partingOf`, which says **which input moved**. Given two
64
+ snapshots carrying holdings it walks the component boundaries for the shallowest
65
+ one whose inputs agreed and whose output did not, and reports that as the origin
66
+ — a moved prop, a context, an external store, or a hook cell by call position.
67
+ It leads with a slice (`variation`, `flake`, `refactor`, `absorbed`, `settled`,
68
+ `unread`) so a reader knows whether to open the rest, and `explainParting` turns
69
+ the whole thing into lines a person reads.
70
+
71
+ ## Smallest working path: compare a value
72
+
73
+ Value comparison needs no host setup and returns paths and fingerprints, not a
74
+ pass/fail verdict:
75
+
76
+ ```ts
77
+ import { compareValues, shapeValue } from '@variance-authority/core';
78
+
79
+ const before = shapeValue({ rows: [{ id: 'a', total: 10 }] }, {
80
+ arrayKey: { '/rows': 'id' },
81
+ });
82
+ const after = shapeValue({ rows: [{ id: 'a', total: 12 }] }, {
83
+ arrayKey: { '/rows': 'id' },
84
+ });
85
+
86
+ const deltas = compareValues(before, after);
87
+ console.log(deltas[0]?.pointer); // /rows/a/total
88
+ ```
89
+
90
+ For a rendered subject, the same package receives captures from a collector:
91
+
92
+ ```ts
93
+ import { attributeRegions, diffSnapshots, isolateRegions, normalize } from '@variance-authority/core';
94
+
95
+ const before = normalize(capture); // supplied by a collector
96
+ const after = normalize(recapture);
97
+ const diff = diffSnapshots(before, after); // deltas, roots, no verdict
98
+
99
+ // `mask` is a ChangeMask from the raster tier. Core never opens an image.
100
+ const places = isolateRegions(mask, { cell: 8 }); // pixels → regions
101
+ const named = attributeRegions(places.regions, after, { scale: 2 });
102
+ ```
103
+
104
+ The value example prints a JSON Pointer for the changed field. The rendered path
105
+ returns semantic roots and, when a raster mask is supplied, regions attributed to
106
+ the candidate snapshot. A caller still chooses policy and a verdict in
107
+ `core/judge` or in its own runner.
108
+
109
+ `scale` is device pixels per CSS pixel, it is **required**, and it has no default
110
+ on purpose: a 2x screenshot attributed at 1x lands every region in the top-left
111
+ quadrant and names the wrong component for each — a full, plausible, entirely
112
+ wrong report. That last step is where regions become components and files.
113
+
114
+ The options that change scope are explicit at the call site:
115
+
116
+ | call | useful controls |
117
+ |---|---|
118
+ | `normalize` | `collapseWrappers` removes layout-only wrapper boundaries, `digestText` includes text content, and `sourceRoot` relativizes source locations |
119
+ | `isolateRegions` / `attributeRegions` | `cell` and `limit` bound mask work; `origin` and `containment` describe the coordinate origin and how much a node must contain a region |
120
+ | `fingerprintOfMask` | `grid` controls the shape sample and `coverage` controls the minimum occupied share |
121
+ | `compareLocales` / ignore validation | `slack` permits a declared locale distance; `sites` supplies resolved ignore locations and `now` evaluates expiry |
122
+ | `buildDocket` / dependency reach | `sampleSize` limits review examples and `through` selects graph edge kinds |
123
+ | report summaries | `source` maps component names to files when `summarizeAdjudication` or `summarizeFindings` needs an actionable path |
124
+ | screenshot stabilization | `animations` and `caret` are explicit intervention settings; omitted means the caller did not assert either intervention |
125
+
126
+ ### A subject that was never rendered
127
+
128
+ `shapeValue(value, options)` is the same treatment for a value — an API
129
+ response, a generated schema, a route table. It canonicalizes (keys sorted,
130
+ numbers written portably, `undefined` omitted), addresses the text by content,
131
+ and returns a `CapturedValue`; `compareValues` turns two of them into deltas
132
+ carrying a JSON Pointer and a fingerprint, and — like everything else here — no
133
+ verdict.
134
+
135
+ ```ts
136
+ import { shapeValue, compareValues } from '@variance-authority/core';
137
+
138
+ const baseline = shapeValue(before, { arrayKey: { '/rows': 'id' } });
139
+ const deltas = compareValues(baseline, shapeValue(after, { arrayKey: { '/rows': 'id' } }));
140
+ ```
141
+
142
+ `drop` records a pointer's value as present without comparing it, `replace` puts
143
+ a token you choose in its place, and `arrayKey` says which member identifies a
144
+ row — the difference between *one row was added* and *two thousand rows moved*.
145
+ Two more options describe the value rather than shape it: `dialect` is how a
146
+ reader will interpret the text later (`'json'` by default, `'openapi'` and
147
+ `'graphql'` being the ones with detectors), and `generator` names what emitted
148
+ it. Neither reaches the digest, so declaring a dialect does not orphan a
149
+ baseline.
150
+
151
+ Non-data throws, naming the pointer. A function, a `Date`, a `bigint` or a
152
+ non-finite number cannot be canonical text, and dropping one silently puts a key
153
+ in the record that the next run reads as removed.
154
+
155
+ Rendered comparisons also throw when the subjects or observation profiles differ.
156
+
157
+ ## What it refuses
158
+
159
+ **Absent is not empty.** Not measured, measured as zero, and unobservable are
160
+ three states, and collapsing any two produces a pass nobody earned. A **band**
161
+ (the frequency category a change falls into — `a11y`, `geometry`, `token`,
162
+ `content`, `texture`, rarest to noisiest) that a profile cannot see reports
163
+ `UNOBSERVED`, which the type system will not let you spell the same way as a
164
+ pass.
165
+
166
+ **Two results whose identities differ are `incomparable`, never `different`.**
167
+ A difference in conditions reported as a difference in the product is a
168
+ confident wrong answer, and the confidence is what makes it expensive.
169
+
@@ -0,0 +1,47 @@
1
+ import type { SourceIndex } from './attribute/source.js';
2
+ import type { AccessibilitySnapshot, CapturedValue, Raster, RenderDocument, SemanticSnapshot, SubjectRef } from './format/index.js';
3
+ /**
4
+ * Material already captured from a host, before retention or reporting.
5
+ *
6
+ * Three arms, and the third is the one that is not a rendering at all: a value
7
+ * has no viewport, no placement and nothing to paint, and it reaches the same
8
+ * attribution path as the other two (ADR-0044). Every existing reader narrows
9
+ * with `=== 'document'`, so a value arm is not a compile error anywhere — which
10
+ * means each of those readers owes an explicit refusal rather than a silent drop.
11
+ */
12
+ export type CaptureMaterial = {
13
+ readonly kind: 'document';
14
+ readonly document: RenderDocument;
15
+ } | {
16
+ readonly kind: 'raster';
17
+ readonly raster: Raster;
18
+ } | {
19
+ readonly kind: 'value';
20
+ readonly value: CapturedValue;
21
+ };
22
+ /**
23
+ * The acquisition boundary shared by host adapters.
24
+ *
25
+ * The material decides whether a renderer is needed. Semantic and source
26
+ * evidence stay beside it so an in-place raster and a deferred document reach
27
+ * the same attribution path without inventing host-specific result types.
28
+ */
29
+ export interface CaptureArtifact {
30
+ readonly artifactVersion: 1;
31
+ readonly subject: SubjectRef;
32
+ readonly material: CaptureMaterial;
33
+ readonly snapshot?: SemanticSnapshot;
34
+ /** Browser-native accessibility evidence from the same stabilized mount. */
35
+ readonly accessibility?: AccessibilitySnapshot;
36
+ readonly source?: SourceIndex;
37
+ readonly stabilization?: readonly string[];
38
+ readonly attempt?: {
39
+ readonly retry: number;
40
+ readonly repeat: number;
41
+ readonly shard?: {
42
+ readonly index: number;
43
+ readonly total: number;
44
+ };
45
+ };
46
+ }
47
+ //# sourceMappingURL=artifact.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=artifact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact.js","sourceRoot":"","sources":["../src/artifact.ts"],"names":[],"mappings":"","sourcesContent":["import type { SourceIndex } from './attribute/source.js';\nimport type {\n AccessibilitySnapshot,\n CapturedValue,\n Raster,\n RenderDocument,\n SemanticSnapshot,\n SubjectRef,\n} from './format/index.js';\n\n/**\n * Material already captured from a host, before retention or reporting.\n *\n * Three arms, and the third is the one that is not a rendering at all: a value\n * has no viewport, no placement and nothing to paint, and it reaches the same\n * attribution path as the other two (ADR-0044). Every existing reader narrows\n * with `=== 'document'`, so a value arm is not a compile error anywhere — which\n * means each of those readers owes an explicit refusal rather than a silent drop.\n */\nexport type CaptureMaterial =\n | { readonly kind: 'document'; readonly document: RenderDocument }\n | { readonly kind: 'raster'; readonly raster: Raster }\n | { readonly kind: 'value'; readonly value: CapturedValue };\n\n/**\n * The acquisition boundary shared by host adapters.\n *\n * The material decides whether a renderer is needed. Semantic and source\n * evidence stay beside it so an in-place raster and a deferred document reach\n * the same attribution path without inventing host-specific result types.\n */\nexport interface CaptureArtifact {\n readonly artifactVersion: 1;\n readonly subject: SubjectRef;\n readonly material: CaptureMaterial;\n readonly snapshot?: SemanticSnapshot;\n /** Browser-native accessibility evidence from the same stabilized mount. */\n readonly accessibility?: AccessibilitySnapshot;\n readonly source?: SourceIndex;\n readonly stabilization?: readonly string[];\n readonly attempt?: {\n readonly retry: number;\n readonly repeat: number;\n readonly shard?: { readonly index: number; readonly total: number };\n };\n}\n"]}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The one sentence a movement carries, written once for both readers.
3
+ *
4
+ * Every rung ends in prose because the category is not the answer — *upstream*
5
+ * tells a reviewer nothing, `ProductCard` was edited and reaches it through
6
+ * `Card` sends them to a file. The sentence is written here, shipped to the
7
+ * terminal and to the review page, and rendered rather than rewritten at the far
8
+ * end: a second sentence composed for the web is a second thing that can
9
+ * disagree with the first, and the disagreement would be between a reviewer's
10
+ * screen and their CI log.
11
+ *
12
+ * Names the sentence claims things about are wrapped in backticks. That is for
13
+ * the terminal, and the page turns each pair into `code` rather than reprinting
14
+ * the punctuation.
15
+ */
16
+ import type { Site } from './composition.js';
17
+ import type { Ancestor, Evidence } from './movement.js';
18
+ /**
19
+ * The upstream sentence, which is the one a reviewer acts on.
20
+ *
21
+ * It says three things and the third is the reason the first two are worth
22
+ * printing: an edited component reaches this one, this one's own file is not in
23
+ * the change set, and therefore what moved here is what it was handed. Every
24
+ * rung above this has already been tried, so *its own code* and *a token it
25
+ * reads* are both ruled out by the time this speaks.
26
+ */
27
+ export declare function becauseUpstream(ancestor: Ancestor): string;
28
+ /**
29
+ * Why nothing explains it, and how much the suite could say about that.
30
+ *
31
+ * The three endings are three different findings and were one sentence. An empty
32
+ * control group has two causes — the component renders nowhere else with these
33
+ * inputs, or it renders elsewhere and moved in every one of them — and they point
34
+ * opposite ways. The first is a suite with nothing to compare against. The second
35
+ * *is* the comparison, and a component that moved in all six of its renders with
36
+ * no file, token or ancestor behind it is the strongest unexplained this ladder
37
+ * can produce.
38
+ */
39
+ export declare function unexplainedBecause(movement: {
40
+ readonly component: string;
41
+ readonly held: readonly Site[];
42
+ }, considered: number, evidence: Evidence): string;
43
+ //# sourceMappingURL=because.d.ts.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The one sentence a movement carries, written once for both readers.
3
+ *
4
+ * Every rung ends in prose because the category is not the answer — *upstream*
5
+ * tells a reviewer nothing, `ProductCard` was edited and reaches it through
6
+ * `Card` sends them to a file. The sentence is written here, shipped to the
7
+ * terminal and to the review page, and rendered rather than rewritten at the far
8
+ * end: a second sentence composed for the web is a second thing that can
9
+ * disagree with the first, and the disagreement would be between a reviewer's
10
+ * screen and their CI log.
11
+ *
12
+ * Names the sentence claims things about are wrapped in backticks. That is for
13
+ * the terminal, and the page turns each pair into `code` rather than reprinting
14
+ * the punctuation.
15
+ */
16
+ /**
17
+ * The upstream sentence, which is the one a reviewer acts on.
18
+ *
19
+ * It says three things and the third is the reason the first two are worth
20
+ * printing: an edited component reaches this one, this one's own file is not in
21
+ * the change set, and therefore what moved here is what it was handed. Every
22
+ * rung above this has already been tried, so *its own code* and *a token it
23
+ * reads* are both ruled out by the time this speaks.
24
+ */
25
+ export function becauseUpstream(ancestor) {
26
+ const reaches = ancestor.through.length === 0 ? 'mounts it' : `reaches it through ${chain(ancestor.through)}`;
27
+ return (`\`${ancestor.name}\` was edited and ${reaches}; nothing edited its own file, ` +
28
+ `so it moved on what it was given`);
29
+ }
30
+ /** The components in between, named while there are few enough to be worth naming. */
31
+ function chain(through) {
32
+ const named = through.map((name) => `\`${name}\``);
33
+ if (named.length === 1)
34
+ return named[0] ?? '';
35
+ if (named.length > 3)
36
+ return `${named.slice(0, 3).join(', ')} and ${String(named.length - 3)} more`;
37
+ return `${named.slice(0, -1).join(', ')} and ${named[named.length - 1] ?? ''}`;
38
+ }
39
+ /**
40
+ * Why nothing explains it, and how much the suite could say about that.
41
+ *
42
+ * The three endings are three different findings and were one sentence. An empty
43
+ * control group has two causes — the component renders nowhere else with these
44
+ * inputs, or it renders elsewhere and moved in every one of them — and they point
45
+ * opposite ways. The first is a suite with nothing to compare against. The second
46
+ * *is* the comparison, and a component that moved in all six of its renders with
47
+ * no file, token or ancestor behind it is the strongest unexplained this ladder
48
+ * can produce.
49
+ */
50
+ export function unexplainedBecause(movement, considered, evidence) {
51
+ if (evidence.changed === undefined) {
52
+ return 'nothing was asked about what changed, so nothing here explains it — run with `--against` to reach the first rung';
53
+ }
54
+ const unexplained = 'no file, token or ancestor explains it, and';
55
+ if (considered === 0) {
56
+ return `${unexplained} it renders nowhere else in this run with these props to compare against`;
57
+ }
58
+ if (movement.held.length === 0) {
59
+ return `${unexplained} every other render of it with these props moved here too`;
60
+ }
61
+ return (`${unexplained} the same component with the same props held in ` +
62
+ `${movement.held.length} of ${considered} other place(s) in this run`);
63
+ }
64
+ //# sourceMappingURL=because.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"because.js","sourceRoot":"","sources":["../../src/attribute/because.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,QAAkB;IAChD,MAAM,OAAO,GACX,QAAQ,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,sBAAsB,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;IAEhG,OAAO,CACL,KAAK,QAAQ,CAAC,IAAI,qBAAqB,OAAO,iCAAiC;QAC/E,kCAAkC,CACnC,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,SAAS,KAAK,CAAC,OAA0B;IACvC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;IACnD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC;IACpG,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;AACjF,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAAwE,EACxE,UAAkB,EAClB,QAAkB;IAElB,IAAI,QAAQ,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,kHAAkH,CAAC;IAC5H,CAAC;IAED,MAAM,WAAW,GAAG,6CAA6C,CAAC;IAElE,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,GAAG,WAAW,0EAA0E,CAAC;IAClG,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,GAAG,WAAW,2DAA2D,CAAC;IACnF,CAAC;IAED,OAAO,CACL,GAAG,WAAW,kDAAkD;QAChE,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,OAAO,UAAU,6BAA6B,CACtE,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The one sentence a movement carries, written once for both readers.\n *\n * Every rung ends in prose because the category is not the answer — *upstream*\n * tells a reviewer nothing, `ProductCard` was edited and reaches it through\n * `Card` sends them to a file. The sentence is written here, shipped to the\n * terminal and to the review page, and rendered rather than rewritten at the far\n * end: a second sentence composed for the web is a second thing that can\n * disagree with the first, and the disagreement would be between a reviewer's\n * screen and their CI log.\n *\n * Names the sentence claims things about are wrapped in backticks. That is for\n * the terminal, and the page turns each pair into `code` rather than reprinting\n * the punctuation.\n */\n\nimport type { Site } from './composition.js';\nimport type { Ancestor, Evidence } from './movement.js';\n\n/**\n * The upstream sentence, which is the one a reviewer acts on.\n *\n * It says three things and the third is the reason the first two are worth\n * printing: an edited component reaches this one, this one's own file is not in\n * the change set, and therefore what moved here is what it was handed. Every\n * rung above this has already been tried, so *its own code* and *a token it\n * reads* are both ruled out by the time this speaks.\n */\nexport function becauseUpstream(ancestor: Ancestor): string {\n const reaches =\n ancestor.through.length === 0 ? 'mounts it' : `reaches it through ${chain(ancestor.through)}`;\n\n return (\n `\\`${ancestor.name}\\` was edited and ${reaches}; nothing edited its own file, ` +\n `so it moved on what it was given`\n );\n}\n\n/** The components in between, named while there are few enough to be worth naming. */\nfunction chain(through: readonly string[]): string {\n const named = through.map((name) => `\\`${name}\\``);\n if (named.length === 1) return named[0] ?? '';\n if (named.length > 3) return `${named.slice(0, 3).join(', ')} and ${String(named.length - 3)} more`;\n return `${named.slice(0, -1).join(', ')} and ${named[named.length - 1] ?? ''}`;\n}\n\n/**\n * Why nothing explains it, and how much the suite could say about that.\n *\n * The three endings are three different findings and were one sentence. An empty\n * control group has two causes — the component renders nowhere else with these\n * inputs, or it renders elsewhere and moved in every one of them — and they point\n * opposite ways. The first is a suite with nothing to compare against. The second\n * *is* the comparison, and a component that moved in all six of its renders with\n * no file, token or ancestor behind it is the strongest unexplained this ladder\n * can produce.\n */\nexport function unexplainedBecause(\n movement: { readonly component: string; readonly held: readonly Site[] },\n considered: number,\n evidence: Evidence,\n): string {\n if (evidence.changed === undefined) {\n return 'nothing was asked about what changed, so nothing here explains it — run with `--against` to reach the first rung';\n }\n\n const unexplained = 'no file, token or ancestor explains it, and';\n\n if (considered === 0) {\n return `${unexplained} it renders nowhere else in this run with these props to compare against`;\n }\n\n if (movement.held.length === 0) {\n return `${unexplained} every other render of it with these props moved here too`;\n }\n\n return (\n `${unexplained} the same component with the same props held in ` +\n `${movement.held.length} of ${considered} other place(s) in this run`\n );\n}\n"]}
@@ -0,0 +1,191 @@
1
+ import type { CanonicalValue } from '../format/canonical.js';
2
+ import type { Rect } from '../format/capture.js';
3
+ import type { Digest } from '../format/hash.js';
4
+ import type { SemanticNode } from '../format/snapshot.js';
5
+ /**
6
+ * Where one component's nodes stop and the next component's begin.
7
+ *
8
+ * Two relations run upward out of a rendered node and they are not the same
9
+ * relation. **Parent** is what is above it — React's `return` chain, the boxes
10
+ * it ended up inside. **Owner** is what rendered it — React's `_debugOwner`, the
11
+ * code that wrote the element. A layout wrapper is the parent of everything
12
+ * handed to it and the owner of none of it.
13
+ *
14
+ * Both are needed here, for different halves of the same answer:
15
+ *
16
+ * - **Parent decides nesting.** A boundary owns a contiguous region of the
17
+ * document, and what encloses that region is a fact about the tree. Reading
18
+ * the owner for this would produce a boundary set that is not a partition.
19
+ * - **Owner decides membership and naming.** A component's own content is what
20
+ * *it* wrote. Content it was handed is a hole in its output — present, sized,
21
+ * positioned by it, and authored somewhere else.
22
+ *
23
+ * Reading the parent for both — which is what a walk over `owners[0]` does — has
24
+ * two consequences, and they are the two shapes a design system is made of:
25
+ *
26
+ * - **A component that renders only components disappears.** It authors no host
27
+ * node, so it is never any node's nearest enclosing composite, so it is a
28
+ * boundary nowhere. That is every variant wrapper (`DangerButton` returning a
29
+ * `Button`) and every page-level assembly. The information is not missing —
30
+ * the composite is in the chain, one rung up — it is discarded by reading only
31
+ * the head of it. {@link boundaries} enters every rung the chain crosses.
32
+ * - **A container absorbs its caller's content.** `Card` renders one `div` and
33
+ * whatever it was given; hashed by enclosure, its digest moves whenever a
34
+ * caller passes something else, while its props digest — which excludes
35
+ * `children` — says its inputs held. {@link shapeOf} names a child boundary
36
+ * only when this component placed it, and leaves an anonymous hole otherwise.
37
+ *
38
+ * The owner half is a development-build artefact: React populates `_debugOwner`
39
+ * from `element._owner`, and a production bundle does not. Absent, both rules
40
+ * degrade to naming everything, which is the enclosure answer — coarser, never
41
+ * wrong in a new direction.
42
+ *
43
+ * Nothing here is exported from the package. The callers are siblings.
44
+ */
45
+ /** A node whose provenance chain broke. Not a filler category; see `RootKind`. */
46
+ export declare const UNATTRIBUTED = "(unattributed)";
47
+ /**
48
+ * Properties whose *computed* value is layout output rather than authored input.
49
+ *
50
+ * The distinction this list exists for is not stylistic. Under a profile with a
51
+ * layout engine the snapshot carries the engine's resolved values, so a block
52
+ * element's computed `height` is whatever its contents made it — and a button
53
+ * two levels down growing by six pixels moves the computed height of every
54
+ * ancestor. Hashed as *style*, that reports every enclosing component as having
55
+ * changed, which is precisely the "area ranks the displaced above the displacer"
56
+ * failure the cause hashes exist to fix, arriving through a different door.
57
+ *
58
+ * Measured on `cases/storybook-case`: one padding edit inside `Button` made
59
+ * `Tokens`, `Stack`, `Card` and the unattributed root all report a moved style
60
+ * hash, so *every* component in every affected story was named a cause.
61
+ *
62
+ * They are folded into `geometry` instead, where "this component's box is a
63
+ * different size" already belongs and where it correctly does not make a cause.
64
+ * Under a profile *without* layout there are no computed values, so these are
65
+ * authored declarations like any other and stay in `style` — that tier has no
66
+ * `geometry` digest to move them to, and a declared `width: 100px` really is the
67
+ * component's own content.
68
+ */
69
+ export declare const LAYOUT_OUTPUT: ReadonlySet<string>;
70
+ export interface Boundary {
71
+ readonly node: SemanticNode;
72
+ readonly component: string;
73
+ /** The enclosing boundary's component, absent on the subject root. */
74
+ readonly within?: string;
75
+ /** Boundaries between this one and the subject root. The root is `0`. */
76
+ readonly depth: number;
77
+ /**
78
+ * This component's rung in the node's ownership stack.
79
+ *
80
+ * Distinct from `depth`, which counts enclosing boundaries. They diverge
81
+ * wherever a subject root has no provenance: the `(unattributed)` container is
82
+ * a boundary at depth 0 holding a stack of one, and the application inside it
83
+ * starts again at rung 0 while sitting at depth 1.
84
+ */
85
+ readonly rung: number;
86
+ /**
87
+ * Digest of the props this boundary received, from its own `OwnerFrame`.
88
+ *
89
+ * Read at this rung rather than from `owners[0]`, which is the innermost
90
+ * component and belongs to whichever boundary is deepest at this node — the
91
+ * distinction only exists because several boundaries can share a root.
92
+ */
93
+ readonly props?: Digest;
94
+ /** The component whose JSX placed this one. Absent on a production build. */
95
+ readonly placedBy?: string;
96
+ }
97
+ /** Whether a node's own content belongs to this boundary rather than one below. */
98
+ export declare function holds(boundary: Boundary, node: SemanticNode): boolean;
99
+ /**
100
+ * Every boundary root in the subject, in document order.
101
+ *
102
+ * A boundary opens at each rung a node's ownership stack adds to its parent's,
103
+ * plus the subject root. Several can open at one node — a page assembly, the
104
+ * card it returns and the stack inside that card all begin at the same element —
105
+ * which is what makes a component that renders only components visible.
106
+ *
107
+ * Collected in a separate pre-order pass rather than discovered during hashing,
108
+ * so that instance order is document order exactly. Discovering them while
109
+ * walking would order them by boundary depth instead, and "the second instance"
110
+ * would mean something different in a nested tree.
111
+ */
112
+ export declare function boundaries(root: SemanticNode): readonly Boundary[];
113
+ export interface Shape {
114
+ readonly structure: CanonicalValue;
115
+ readonly semantics: CanonicalValue;
116
+ readonly text: CanonicalValue;
117
+ readonly style: CanonicalValue;
118
+ readonly geometry: CanonicalValue;
119
+ /**
120
+ * How the framework holds this boundary, per node that reports it.
121
+ *
122
+ * Its own band because it answers its own question. `structure` through
123
+ * `geometry` all read what the renderer produced; this reads what the
124
+ * component *is* — its hook shape, its wrappers, the contexts it subscribes
125
+ * to, the keys it is reconciled under. Two components can agree on all five
126
+ * content bands and disagree here, and when they do, they behave differently
127
+ * under every change that follows.
128
+ */
129
+ readonly wiring: CanonicalValue;
130
+ /**
131
+ * The box this boundary's root node occupies. Not part of any digest.
132
+ *
133
+ * Beside the digests rather than inside `geometry`, which is the whole subtree:
134
+ * this is the only thing on the record that can say a control grew eight pixels
135
+ * rather than merely stopped being what it was. `null` where nothing was laid out.
136
+ */
137
+ readonly box: Rect | null;
138
+ /** Child boundaries encountered, in document order. Not part of any digest. */
139
+ readonly renders: readonly string[];
140
+ /** Nodes this boundary owns, counting its own root. Not part of any digest. */
141
+ readonly nodes: number;
142
+ /**
143
+ * Custom properties this boundary's own nodes resolved through, sorted.
144
+ *
145
+ * Not part of any digest — the *values* are already inside `style`, and the
146
+ * names are carried beside it so a token that moved can be joined to the
147
+ * components that read it. Off the boundary rather than off the subject on
148
+ * purpose: every subject on a themed page resolves through every token in the
149
+ * theme, so a subject-level list names them all and explains nothing.
150
+ */
151
+ readonly tokens: readonly string[];
152
+ }
153
+ /**
154
+ * How a value that may carry a structural alias is rewritten before hashing.
155
+ *
156
+ * `undefined` for the per-name hashes, which have always hashed the alias the
157
+ * normalizer assigned and must keep doing so byte for byte. Supplied by the
158
+ * per-instance hashes, which need a boundary-local alias space — see
159
+ * {@link ./instances.js}, where the argument for it lives.
160
+ */
161
+ export interface Rename {
162
+ readonly attribute: (name: string, value: string) => string;
163
+ readonly style: (value: string) => string;
164
+ readonly alias: (alias: string) => string;
165
+ }
166
+ /**
167
+ * One boundary's content, stopping at nested boundaries.
168
+ *
169
+ * Where a child belongs to another component, this records a placeholder rather
170
+ * than descending. That is the whole design: a component's hash moves when *its
171
+ * own* code changes, and what a child renders internally is not that. Hashing
172
+ * whole subtrees instead would move every ancestor on any leaf edit, and the
173
+ * page root would change on every commit.
174
+ *
175
+ * **The placeholder names the child only when this component placed it.** A
176
+ * component that wrote `<Stack>` owns that choice, and swapping it for a `<Card>`
177
+ * is its own change; a container that was handed a `<Stack>` chose nothing, and
178
+ * naming it would make the container's digest a function of its callers. So a
179
+ * slot is anonymous, and `Card` renders the same bytes on every page that uses
180
+ * it, whatever it was given. The cost is stated in ADR-0035: the *number* of
181
+ * slotted children still reaches the digest, so a caller passing three where it
182
+ * passed two moves the container.
183
+ *
184
+ * Falls back to naming every child where `_debugOwner` is absent, which is the
185
+ * enclosure answer this replaced.
186
+ *
187
+ * Paths are not hashed. A path is an address that shifts when an unrelated
188
+ * sibling is inserted, so hashing one reports a change nobody made.
189
+ */
190
+ export declare function shapeOf(boundary: Boundary, layout: boolean, rename?: Rename): Shape;
191
+ //# sourceMappingURL=boundary.d.ts.map