@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,312 @@
1
+ import { BANDS } from '../compare/band.js';
2
+ import { digestValue } from '../format/hash.js';
3
+ import { boundaries, shapeOf, UNATTRIBUTED } from './boundary.js';
4
+ export { UNATTRIBUTED } from './boundary.js';
5
+ /**
6
+ * Per-component content hashes, one per band.
7
+ *
8
+ * `SemanticSnapshot` hashes a whole subject. That answers "did anything change"
9
+ * and nothing else: it moves whenever anything inside it moves, so it cannot say
10
+ * *which area*, and a record built on it says "the page changed" on every commit.
11
+ *
12
+ * These hashes are the unit a history is kept in. Text, and small — a component
13
+ * that did not change contributes the same digest it did last time, so the only
14
+ * thing worth recording is the difference.
15
+ */
16
+ /**
17
+ * Hash every component boundary in a subject.
18
+ *
19
+ * Ordered by component name so an unchanged subject produces byte-identical
20
+ * output across runs.
21
+ *
22
+ * The walk and the per-boundary shape live in {@link ./boundary.js}, which
23
+ * {@link ./instances.js} shares — one definition of what a component's own
24
+ * content is, read by the aggregate and by the per-instance form.
25
+ */
26
+ export function hashComponents(snapshot) {
27
+ const layout = snapshot.profile.layout;
28
+ const accumulated = new Map();
29
+ for (const boundary of boundaries(snapshot.root)) {
30
+ const shape = shapeOf(boundary, layout);
31
+ const entry = accumulated.get(boundary.component) ?? {
32
+ structure: [],
33
+ semantics: [],
34
+ text: [],
35
+ style: [],
36
+ geometry: [],
37
+ boxes: [],
38
+ };
39
+ entry.structure.push(shape.structure);
40
+ entry.semantics.push(shape.semantics);
41
+ entry.text.push(shape.text);
42
+ entry.style.push(shape.style);
43
+ entry.geometry.push(shape.geometry);
44
+ entry.boxes.push(shape.box);
45
+ accumulated.set(boundary.component, entry);
46
+ }
47
+ return [...accumulated.entries()]
48
+ .map(([component, entry]) => ({
49
+ component,
50
+ instances: entry.structure.length,
51
+ structure: digestValue(entry.structure),
52
+ semantics: digestValue(entry.semantics),
53
+ text: digestValue(entry.text),
54
+ style: digestValue(entry.style),
55
+ ...(layout ? { geometry: digestValue(entry.geometry) } : {}),
56
+ // Under a profile with no layout every entry is `null`, and a list of
57
+ // nulls is a measurement nobody took written as one they did. Absent
58
+ // instead, which is the same rule `geometry` above obeys.
59
+ ...(layout ? { boxes: entry.boxes } : {}),
60
+ }))
61
+ // Code-unit order, not `localeCompare`. The doc above promises byte-identical
62
+ // output for an unchanged subject, and a locale-aware comparison makes that a
63
+ // promise about the machine's `LANG` — which was tolerable while these were
64
+ // internal and is not now that they are written into a sidecar, committed
65
+ // beside a baseline, and read back on someone else's runner.
66
+ .sort((a, b) => (a.component < b.component ? -1 : a.component > b.component ? 1 : 0));
67
+ }
68
+ /**
69
+ * Which components *caused* a change, given both revisions' hashes.
70
+ *
71
+ * The arithmetic behind cause-first ranking, and the reason a baseline carries
72
+ * its component hashes at all. `rankRegions` takes a list of cause names and,
73
+ * given none, falls back to area — which
74
+ * [journal 0013](../../../../docs/context/journal/0013-observability.md)
75
+ * measured as backwards by 6×, because area measures displacement rather than
76
+ * cause. This is where that list comes from on a path with no second document.
77
+ *
78
+ * The distinction is the split ADR-0018 built the bands for:
79
+ *
80
+ * - **`structure` or `style` moved** — the component's own content is different.
81
+ * It is a cause.
82
+ * - **only `geometry` moved** — the component is byte-identical and its box is
83
+ * somewhere else. Something *else* moved it, so it is collateral, and naming
84
+ * it would send a reviewer to a file nobody edited.
85
+ * - **appeared or disappeared** — a component present on one side only is a
86
+ * cause. Something decided to render it or to stop.
87
+ *
88
+ * `instances` is deliberately not consulted. A component rendered five times
89
+ * instead of four has a different `structure` digest for the subject, and the
90
+ * count moving on its own — the same component, the same content, one more of
91
+ * them — is a change in whatever decided how many, not in this component.
92
+ *
93
+ * A profile with no layout supplies no `geometry`, so on that tier every
94
+ * difference is `structure` or `style` and every changed component is a cause.
95
+ * That is correct rather than degraded: with no boxes, nothing was displaced.
96
+ */
97
+ export function causesBetween(before, after) {
98
+ const previous = new Map(before.map((entry) => [entry.component, entry]));
99
+ const causes = [];
100
+ for (const entry of after) {
101
+ // `(unattributed)` is not a component and can never be a cause. It is the
102
+ // bucket for nodes whose provenance chain broke, so it collects unrelated
103
+ // parts of a page under one name — and nothing downstream could act on it
104
+ // anyway: a region with no owner reports no component, so it would never
105
+ // match. A broken chain is a defect in this tool and is reported as
106
+ // `unattributed` where that means something, not smuggled in here as a
107
+ // culprit.
108
+ if (entry.component === UNATTRIBUTED)
109
+ continue;
110
+ const was = previous.get(entry.component);
111
+ if (was === undefined) {
112
+ causes.push(entry.component);
113
+ continue;
114
+ }
115
+ if (ownContentMoved(was, entry))
116
+ causes.push(entry.component);
117
+ }
118
+ // Removals too, and they are the case a candidate-only walk cannot see: a
119
+ // component that stopped rendering leaves regions behind it, and the component
120
+ // that used to be there is exactly the name a reviewer needs.
121
+ const present = new Set(after.map((entry) => entry.component));
122
+ for (const entry of before) {
123
+ if (entry.component === UNATTRIBUTED)
124
+ continue;
125
+ if (!present.has(entry.component))
126
+ causes.push(entry.component);
127
+ }
128
+ return causes.sort();
129
+ }
130
+ /**
131
+ * Whether a component's *own content* differs, ignoring where its box ended up.
132
+ *
133
+ * Deliberately not phrased in bands, because it is not a band question. Both
134
+ * `structure` and `geometry` map to the `geometry` band, and this has to keep
135
+ * them apart: a component whose tree changed edited itself, and a component
136
+ * whose rect moved was pushed. That distinction is the entire cause/collateral
137
+ * result, and asking it through the band mapping would need the digests back
138
+ * again to answer it.
139
+ *
140
+ * The list is every digest a component owns except `geometry`. It grew by two on
141
+ * 2026-08-06 without changing meaning: `semantics` and `text` used to be inside
142
+ * `structure`.
143
+ */
144
+ function ownContentMoved(before, after) {
145
+ return (before.structure !== after.structure ||
146
+ before.semantics !== after.semantics ||
147
+ before.text !== after.text ||
148
+ before.style !== after.style);
149
+ }
150
+ /**
151
+ * Which frequency bands moved between one component's two hashes.
152
+ *
153
+ * The reason the digests were split. A baseline carries hashes and not
154
+ * documents, so "what changed here" used to be answerable only as a boolean —
155
+ * and a boolean cannot serve a route-level test, whose entire request is *tell
156
+ * me when the page stops assembling and never when it is repainted*.
157
+ *
158
+ * The mapping is exact and it is the same one `bandOf` applies to a delta, which
159
+ * is the property that matters: a subject relaxed to `layout` must absorb the
160
+ * same things whether the run held two documents or two sidecars. Two mappings
161
+ * would be one drift away from a config key meaning different things on the two
162
+ * paths, discovered as a regression somebody let through.
163
+ *
164
+ * | digest | band | what it covers |
165
+ * |---|---|---|
166
+ * | `semantics` | `a11y` | role, accessible name, ARIA state |
167
+ * | `text` | `content` | text runs |
168
+ * | `structure` | `geometry` | tags, aliases, attributes, child boundaries |
169
+ * | `geometry` | `geometry` | rects and computed layout output |
170
+ * | `style` | `token` | declared values and custom properties |
171
+ *
172
+ * `texture` never appears. It is raster residue by definition, and a component
173
+ * hash is built from a document — so the band a comparison of hashes cannot
174
+ * decide is *absent* from the answer rather than reported as unmoved, which is
175
+ * ADR-0002's rule applied to a narrower question.
176
+ *
177
+ * A missing `geometry` on either side is the profile saying it has no layout
178
+ * engine, and is not a difference. Treating absent as a change would report
179
+ * every component as having moved the moment a jsdom baseline met a Chromium
180
+ * run — which the environment key already refuses as `incomparable`, so this
181
+ * would be a second, wronger answer to a question already settled.
182
+ */
183
+ export function movedBands(before, after) {
184
+ const moved = new Set();
185
+ if (before.semantics !== after.semantics)
186
+ moved.add('a11y');
187
+ if (before.text !== after.text)
188
+ moved.add('content');
189
+ if (before.structure !== after.structure)
190
+ moved.add('geometry');
191
+ if (before.style !== after.style)
192
+ moved.add('token');
193
+ if (before.geometry !== undefined &&
194
+ after.geometry !== undefined &&
195
+ before.geometry !== after.geometry) {
196
+ moved.add('geometry');
197
+ }
198
+ return BANDS.filter((band) => moved.has(band));
199
+ }
200
+ /**
201
+ * Every band that moved anywhere in the subject, given both revisions' hashes.
202
+ *
203
+ * A component present on one side only contributes `geometry`: something was
204
+ * added or removed, which is the structural half of that band however the rest
205
+ * of it compares. It deliberately does not contribute `a11y` or `content` as
206
+ * well — a component that is simply not there did not *rename* anything, and
207
+ * inflating the answer would make a level that absorbs nothing look like the
208
+ * only safe choice.
209
+ */
210
+ export function bandsBetween(before, after) {
211
+ const previous = new Map(before.map((entry) => [entry.component, entry]));
212
+ const present = new Set(after.map((entry) => entry.component));
213
+ const moved = new Set();
214
+ for (const entry of after) {
215
+ const was = previous.get(entry.component);
216
+ if (was === undefined)
217
+ moved.add('geometry');
218
+ else
219
+ for (const band of movedBands(was, entry))
220
+ moved.add(band);
221
+ }
222
+ for (const entry of before) {
223
+ if (!present.has(entry.component))
224
+ moved.add('geometry');
225
+ }
226
+ return BANDS.filter((band) => moved.has(band));
227
+ }
228
+ /**
229
+ * Every component whose hashes differ, with the bands it differs in.
230
+ *
231
+ * One-sided components contribute `geometry` and nothing else, which is
232
+ * {@link bandsBetween}'s rule and must stay identical to it: a component that is
233
+ * simply not there did not rename anything, and inflating the answer would put
234
+ * an `a11y` claim on a page for a component nobody can look at. `presence` is
235
+ * what carries the rest of that meaning, so no reader has to infer *appeared*
236
+ * from a lone `geometry`.
237
+ *
238
+ * `(unattributed)` is excluded for the reason {@link causesBetween} excludes it:
239
+ * it is a bucket for nodes whose provenance chain broke, so it collects
240
+ * unrelated parts of a page under one name and nothing downstream could act on
241
+ * it. The broken chain is reported where it means something, not here as a
242
+ * component that moved.
243
+ */
244
+ export function movedBandsBetween(before, after) {
245
+ const previous = new Map(before.map((entry) => [entry.component, entry]));
246
+ const present = new Set(after.map((entry) => entry.component));
247
+ const moved = [];
248
+ for (const entry of after) {
249
+ if (entry.component === UNATTRIBUTED)
250
+ continue;
251
+ const was = previous.get(entry.component);
252
+ if (was === undefined) {
253
+ moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'added' });
254
+ continue;
255
+ }
256
+ const bands = movedBands(was, entry);
257
+ if (bands.length === 0)
258
+ continue;
259
+ const grew = grewBetween(was.boxes, entry.boxes);
260
+ moved.push({
261
+ component: entry.component,
262
+ bands,
263
+ cause: ownContentMoved(was, entry),
264
+ ...(grew === undefined ? {} : { grew }),
265
+ });
266
+ }
267
+ for (const entry of before) {
268
+ if (entry.component === UNATTRIBUTED)
269
+ continue;
270
+ if (present.has(entry.component))
271
+ continue;
272
+ moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'removed' });
273
+ }
274
+ return moved.sort((left, right) => left.component.localeCompare(right.component));
275
+ }
276
+ /**
277
+ * One size delta both sides agree on, or nothing.
278
+ *
279
+ * Instances are paired by document order, which is the only order either side
280
+ * has. That pairing is sound exactly while the counts match: a component that
281
+ * gained an instance shifted every index after the insertion, and the deltas
282
+ * that fell out would be measurements of one instance against a different one.
283
+ * So a changed count answers nothing rather than answering wrongly — the count
284
+ * itself already moved `structure`, and the reviewer is told that instead.
285
+ *
286
+ * Instances that disagree also answer nothing. Three buttons where one grew and
287
+ * two did not is a real finding and it is not *this* one, and printing the first
288
+ * or the largest would be the page picking a representative and not saying so.
289
+ *
290
+ * Position is deliberately not read. Everything below a control that got taller
291
+ * moved down, and a delta drawn from `x`/`y` would name every one of them.
292
+ */
293
+ function grewBetween(before, after) {
294
+ if (before === undefined || after === undefined)
295
+ return undefined;
296
+ if (before.length === 0 || before.length !== after.length)
297
+ return undefined;
298
+ let agreed;
299
+ for (const [index, was] of before.entries()) {
300
+ const now = after[index];
301
+ if (was === null || was === undefined || now === null || now === undefined)
302
+ return undefined;
303
+ const width = now.width - was.width;
304
+ const height = now.height - was.height;
305
+ if (agreed === undefined)
306
+ agreed = { width, height };
307
+ else if (agreed.width !== width || agreed.height !== height)
308
+ return undefined;
309
+ }
310
+ return agreed === undefined || (agreed.width === 0 && agreed.height === 0) ? undefined : agreed;
311
+ }
312
+ //# sourceMappingURL=component-hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component-hash.js","sourceRoot":"","sources":["../../src/attribute/component-hash.ts"],"names":[],"mappings":"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","sourcesContent":["import { BANDS, type Band } from '../compare/band.js';\nimport type { CanonicalValue } from '../format/canonical.js';\nimport type { Rect } from '../format/capture.js';\nimport type { Digest } from '../format/hash.js';\nimport { digestValue } from '../format/hash.js';\nimport type { ComponentHash, SemanticSnapshot } from '../format/snapshot.js';\nimport { boundaries, shapeOf, UNATTRIBUTED } from './boundary.js';\n\nexport type { ComponentHash } from '../format/snapshot.js';\nexport { UNATTRIBUTED } from './boundary.js';\n\n/**\n * The digests a comparison of two component states actually reads.\n *\n * `movedBands` used to take two `ComponentHash`es, which carry a name and an\n * instance count neither side of the comparison consults. Narrowing it to the\n * digests is what lets a per-instance record (`ComponentInstance`, which has no\n * `instances` count because it *is* one) be compared by the same function —\n * rather than by a second copy of the band mapping, which is the one thing in\n * this file that must not exist twice: a subject relaxed to `layout` has to\n * absorb the same bands whichever shape the caller happened to be holding.\n */\nexport interface BandDigests {\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n readonly geometry?: Digest;\n}\n\n/**\n * Per-component content hashes, one per band.\n *\n * `SemanticSnapshot` hashes a whole subject. That answers \"did anything change\"\n * and nothing else: it moves whenever anything inside it moves, so it cannot say\n * *which area*, and a record built on it says \"the page changed\" on every commit.\n *\n * These hashes are the unit a history is kept in. Text, and small — a component\n * that did not change contributes the same digest it did last time, so the only\n * thing worth recording is the difference.\n */\n\n/**\n * Hash every component boundary in a subject.\n *\n * Ordered by component name so an unchanged subject produces byte-identical\n * output across runs.\n *\n * The walk and the per-boundary shape live in {@link ./boundary.js}, which\n * {@link ./instances.js} shares — one definition of what a component's own\n * content is, read by the aggregate and by the per-instance form.\n */\nexport function hashComponents(snapshot: SemanticSnapshot): readonly ComponentHash[] {\n const layout = snapshot.profile.layout;\n\n const accumulated = new Map<\n string,\n {\n structure: CanonicalValue[];\n semantics: CanonicalValue[];\n text: CanonicalValue[];\n style: CanonicalValue[];\n geometry: CanonicalValue[];\n boxes: (Rect | null)[];\n }\n >();\n\n for (const boundary of boundaries(snapshot.root)) {\n const shape = shapeOf(boundary, layout);\n\n const entry = accumulated.get(boundary.component) ?? {\n structure: [],\n semantics: [],\n text: [],\n style: [],\n geometry: [],\n boxes: [],\n };\n entry.structure.push(shape.structure);\n entry.semantics.push(shape.semantics);\n entry.text.push(shape.text);\n entry.style.push(shape.style);\n entry.geometry.push(shape.geometry);\n entry.boxes.push(shape.box);\n accumulated.set(boundary.component, entry);\n }\n\n return [...accumulated.entries()]\n .map(([component, entry]) => ({\n component,\n instances: entry.structure.length,\n structure: digestValue(entry.structure),\n semantics: digestValue(entry.semantics),\n text: digestValue(entry.text),\n style: digestValue(entry.style),\n ...(layout ? { geometry: digestValue(entry.geometry) } : {}),\n // Under a profile with no layout every entry is `null`, and a list of\n // nulls is a measurement nobody took written as one they did. Absent\n // instead, which is the same rule `geometry` above obeys.\n ...(layout ? { boxes: entry.boxes } : {}),\n }))\n // Code-unit order, not `localeCompare`. The doc above promises byte-identical\n // output for an unchanged subject, and a locale-aware comparison makes that a\n // promise about the machine's `LANG` — which was tolerable while these were\n // internal and is not now that they are written into a sidecar, committed\n // beside a baseline, and read back on someone else's runner.\n .sort((a, b) => (a.component < b.component ? -1 : a.component > b.component ? 1 : 0));\n}\n\n/**\n * Which components *caused* a change, given both revisions' hashes.\n *\n * The arithmetic behind cause-first ranking, and the reason a baseline carries\n * its component hashes at all. `rankRegions` takes a list of cause names and,\n * given none, falls back to area — which\n * [journal 0013](../../../../docs/context/journal/0013-observability.md)\n * measured as backwards by 6×, because area measures displacement rather than\n * cause. This is where that list comes from on a path with no second document.\n *\n * The distinction is the split ADR-0018 built the bands for:\n *\n * - **`structure` or `style` moved** — the component's own content is different.\n * It is a cause.\n * - **only `geometry` moved** — the component is byte-identical and its box is\n * somewhere else. Something *else* moved it, so it is collateral, and naming\n * it would send a reviewer to a file nobody edited.\n * - **appeared or disappeared** — a component present on one side only is a\n * cause. Something decided to render it or to stop.\n *\n * `instances` is deliberately not consulted. A component rendered five times\n * instead of four has a different `structure` digest for the subject, and the\n * count moving on its own — the same component, the same content, one more of\n * them — is a change in whatever decided how many, not in this component.\n *\n * A profile with no layout supplies no `geometry`, so on that tier every\n * difference is `structure` or `style` and every changed component is a cause.\n * That is correct rather than degraded: with no boxes, nothing was displaced.\n */\nexport function causesBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly string[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const causes: string[] = [];\n\n for (const entry of after) {\n // `(unattributed)` is not a component and can never be a cause. It is the\n // bucket for nodes whose provenance chain broke, so it collects unrelated\n // parts of a page under one name — and nothing downstream could act on it\n // anyway: a region with no owner reports no component, so it would never\n // match. A broken chain is a defect in this tool and is reported as\n // `unattributed` where that means something, not smuggled in here as a\n // culprit.\n if (entry.component === UNATTRIBUTED) continue;\n\n const was = previous.get(entry.component);\n if (was === undefined) {\n causes.push(entry.component);\n continue;\n }\n if (ownContentMoved(was, entry)) causes.push(entry.component);\n }\n\n // Removals too, and they are the case a candidate-only walk cannot see: a\n // component that stopped rendering leaves regions behind it, and the component\n // that used to be there is exactly the name a reviewer needs.\n const present = new Set(after.map((entry) => entry.component));\n for (const entry of before) {\n if (entry.component === UNATTRIBUTED) continue;\n if (!present.has(entry.component)) causes.push(entry.component);\n }\n\n return causes.sort();\n}\n\n/**\n * Whether a component's *own content* differs, ignoring where its box ended up.\n *\n * Deliberately not phrased in bands, because it is not a band question. Both\n * `structure` and `geometry` map to the `geometry` band, and this has to keep\n * them apart: a component whose tree changed edited itself, and a component\n * whose rect moved was pushed. That distinction is the entire cause/collateral\n * result, and asking it through the band mapping would need the digests back\n * again to answer it.\n *\n * The list is every digest a component owns except `geometry`. It grew by two on\n * 2026-08-06 without changing meaning: `semantics` and `text` used to be inside\n * `structure`.\n */\nfunction ownContentMoved(before: BandDigests, after: BandDigests): boolean {\n return (\n before.structure !== after.structure ||\n before.semantics !== after.semantics ||\n before.text !== after.text ||\n before.style !== after.style\n );\n}\n\n/**\n * Which frequency bands moved between one component's two hashes.\n *\n * The reason the digests were split. A baseline carries hashes and not\n * documents, so \"what changed here\" used to be answerable only as a boolean —\n * and a boolean cannot serve a route-level test, whose entire request is *tell\n * me when the page stops assembling and never when it is repainted*.\n *\n * The mapping is exact and it is the same one `bandOf` applies to a delta, which\n * is the property that matters: a subject relaxed to `layout` must absorb the\n * same things whether the run held two documents or two sidecars. Two mappings\n * would be one drift away from a config key meaning different things on the two\n * paths, discovered as a regression somebody let through.\n *\n * | digest | band | what it covers |\n * |---|---|---|\n * | `semantics` | `a11y` | role, accessible name, ARIA state |\n * | `text` | `content` | text runs |\n * | `structure` | `geometry` | tags, aliases, attributes, child boundaries |\n * | `geometry` | `geometry` | rects and computed layout output |\n * | `style` | `token` | declared values and custom properties |\n *\n * `texture` never appears. It is raster residue by definition, and a component\n * hash is built from a document — so the band a comparison of hashes cannot\n * decide is *absent* from the answer rather than reported as unmoved, which is\n * ADR-0002's rule applied to a narrower question.\n *\n * A missing `geometry` on either side is the profile saying it has no layout\n * engine, and is not a difference. Treating absent as a change would report\n * every component as having moved the moment a jsdom baseline met a Chromium\n * run — which the environment key already refuses as `incomparable`, so this\n * would be a second, wronger answer to a question already settled.\n */\nexport function movedBands(before: BandDigests, after: BandDigests): readonly Band[] {\n const moved = new Set<Band>();\n\n if (before.semantics !== after.semantics) moved.add('a11y');\n if (before.text !== after.text) moved.add('content');\n if (before.structure !== after.structure) moved.add('geometry');\n if (before.style !== after.style) moved.add('token');\n if (\n before.geometry !== undefined &&\n after.geometry !== undefined &&\n before.geometry !== after.geometry\n ) {\n moved.add('geometry');\n }\n\n return BANDS.filter((band) => moved.has(band));\n}\n\n/**\n * Every band that moved anywhere in the subject, given both revisions' hashes.\n *\n * A component present on one side only contributes `geometry`: something was\n * added or removed, which is the structural half of that band however the rest\n * of it compares. It deliberately does not contribute `a11y` or `content` as\n * well — a component that is simply not there did not *rename* anything, and\n * inflating the answer would make a level that absorbs nothing look like the\n * only safe choice.\n */\nexport function bandsBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly Band[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const present = new Set(after.map((entry) => entry.component));\n const moved = new Set<Band>();\n\n for (const entry of after) {\n const was = previous.get(entry.component);\n if (was === undefined) moved.add('geometry');\n else for (const band of movedBands(was, entry)) moved.add(band);\n }\n\n for (const entry of before) {\n if (!present.has(entry.component)) moved.add('geometry');\n }\n\n return BANDS.filter((band) => moved.has(band));\n}\n\n/**\n * Which bands moved, kept per component instead of folded into one list.\n *\n * {@link bandsBetween} answers *what kind of change is in this subject*, which is\n * the question a sensitivity level asks. It cannot answer the one a reviewer\n * asks — *what changed, and where* — because the fold is lossy in exactly the\n * place attribution lives: a subject reporting `content, geometry, token` has\n * told you a colour and a string and a size all moved somewhere in it, and left\n * you to guess which of the forty components on the page owns which.\n *\n * Unfolded, the same two sidecars say `Button — geometry, token` and\n * `CardFooter — a11y, content`, and that is a sentence a page can print beside a\n * picture. It is also the record that survives when the raster tier loses the\n * name: a difference that reflowed its neighbours merges into one blob whose box\n * fits no component, so the region resolves to the document root and the edit\n * arrives unattributed — while the hashes, which never looked at a pixel, still\n * hold the component that moved and the sense in which it moved.\n *\n * `cause` repeats the {@link causesBetween} predicate rather than being derived\n * from `bands`, and the repetition is the point: `structure` and `geometry` both\n * map to the `geometry` band, so a component that edited its own tree and one\n * that was merely pushed by a neighbour are indistinguishable *after* the band\n * mapping. Losing that here would make every reflowed container a culprit.\n */\nexport interface ComponentBands {\n readonly component: string;\n /** Non-empty: a component whose digests all matched has no entry at all. */\n readonly bands: readonly Band[];\n /** Its own content moved, as opposed to only its rect. `causesBetween`'s test. */\n readonly cause: boolean;\n /** Set only when the component is on one side alone, which `bands` cannot say. */\n readonly presence?: 'added' | 'removed';\n\n /**\n * How much bigger its own box got, when every instance agrees on the answer.\n *\n * The band a reviewer actually wanted. `geometry` says a rect under here is\n * not the rect it was; this says the control is eight pixels taller and\n * thirty-six wider, which is the padding somebody edited, arriving as a\n * measurement rather than as a guess about which property produced it.\n *\n * Absent for four different reasons and they are one reason: nobody can say.\n * No boxes on one side, a different number of instances, instances that\n * disagree about the delta, or a box that did not change size. Present is\n * always non-zero on at least one axis.\n */\n readonly grew?: { readonly width: number; readonly height: number };\n}\n\n/**\n * Every component whose hashes differ, with the bands it differs in.\n *\n * One-sided components contribute `geometry` and nothing else, which is\n * {@link bandsBetween}'s rule and must stay identical to it: a component that is\n * simply not there did not rename anything, and inflating the answer would put\n * an `a11y` claim on a page for a component nobody can look at. `presence` is\n * what carries the rest of that meaning, so no reader has to infer *appeared*\n * from a lone `geometry`.\n *\n * `(unattributed)` is excluded for the reason {@link causesBetween} excludes it:\n * it is a bucket for nodes whose provenance chain broke, so it collects\n * unrelated parts of a page under one name and nothing downstream could act on\n * it. The broken chain is reported where it means something, not here as a\n * component that moved.\n */\nexport function movedBandsBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly ComponentBands[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const present = new Set(after.map((entry) => entry.component));\n const moved: ComponentBands[] = [];\n\n for (const entry of after) {\n if (entry.component === UNATTRIBUTED) continue;\n const was = previous.get(entry.component);\n if (was === undefined) {\n moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'added' });\n continue;\n }\n const bands = movedBands(was, entry);\n if (bands.length === 0) continue;\n const grew = grewBetween(was.boxes, entry.boxes);\n moved.push({\n component: entry.component,\n bands,\n cause: ownContentMoved(was, entry),\n ...(grew === undefined ? {} : { grew }),\n });\n }\n\n for (const entry of before) {\n if (entry.component === UNATTRIBUTED) continue;\n if (present.has(entry.component)) continue;\n moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'removed' });\n }\n\n return moved.sort((left, right) => left.component.localeCompare(right.component));\n}\n\n/**\n * One size delta both sides agree on, or nothing.\n *\n * Instances are paired by document order, which is the only order either side\n * has. That pairing is sound exactly while the counts match: a component that\n * gained an instance shifted every index after the insertion, and the deltas\n * that fell out would be measurements of one instance against a different one.\n * So a changed count answers nothing rather than answering wrongly — the count\n * itself already moved `structure`, and the reviewer is told that instead.\n *\n * Instances that disagree also answer nothing. Three buttons where one grew and\n * two did not is a real finding and it is not *this* one, and printing the first\n * or the largest would be the page picking a representative and not saying so.\n *\n * Position is deliberately not read. Everything below a control that got taller\n * moved down, and a delta drawn from `x`/`y` would name every one of them.\n */\nfunction grewBetween(\n before: readonly (Rect | null)[] | undefined,\n after: readonly (Rect | null)[] | undefined,\n): { readonly width: number; readonly height: number } | undefined {\n if (before === undefined || after === undefined) return undefined;\n if (before.length === 0 || before.length !== after.length) return undefined;\n\n let agreed: { width: number; height: number } | undefined;\n for (const [index, was] of before.entries()) {\n const now = after[index];\n if (was === null || was === undefined || now === null || now === undefined) return undefined;\n\n const width = now.width - was.width;\n const height = now.height - was.height;\n if (agreed === undefined) agreed = { width, height };\n else if (agreed.width !== width || agreed.height !== height) return undefined;\n }\n\n return agreed === undefined || (agreed.width === 0 && agreed.height === 0) ? undefined : agreed;\n}\n"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * One small suite, built twice over: as a graph, and as something that moved.
3
+ *
4
+ * `composition.test.ts` proves the fold and `movement.test.ts` proves the ladder
5
+ * that reads it, and they need the same three subjects to do it. A chip in its
6
+ * own story and the same chip in a page is the smallest arrangement in which an
7
+ * echo, a divergence and a control group are all available at once, so it is
8
+ * described once here rather than diverging in two files.
9
+ */
10
+ import type { ComponentInstance } from './instances.js';
11
+ import type { SubjectComposition } from './composition.js';
12
+ export declare function instance(over: Partial<ComponentInstance> & {
13
+ component: string;
14
+ }): ComponentInstance;
15
+ /** A chip, rendered the same way, with the same inputs. */
16
+ export declare const chip: (path: string, within: string, over?: Partial<ComponentInstance>) => ComponentInstance;
17
+ export declare const SUITE: readonly SubjectComposition[];
18
+ //# sourceMappingURL=composition-fixture.d.ts.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * One small suite, built twice over: as a graph, and as something that moved.
3
+ *
4
+ * `composition.test.ts` proves the fold and `movement.test.ts` proves the ladder
5
+ * that reads it, and they need the same three subjects to do it. A chip in its
6
+ * own story and the same chip in a page is the smallest arrangement in which an
7
+ * echo, a divergence and a control group are all available at once, so it is
8
+ * described once here rather than diverging in two files.
9
+ */
10
+ export function instance(over) {
11
+ return {
12
+ path: '0',
13
+ depth: 1,
14
+ rendering: 'v1:r',
15
+ structure: 'v1:s',
16
+ semantics: 'v1:a',
17
+ text: 'v1:t',
18
+ style: 'v1:y',
19
+ renders: [],
20
+ nodes: 1,
21
+ tokens: [],
22
+ ...over,
23
+ };
24
+ }
25
+ /** A chip, rendered the same way, with the same inputs. */
26
+ export const chip = (path, within, over = {}) => instance({
27
+ component: 'Chip',
28
+ path,
29
+ within,
30
+ depth: 2,
31
+ props: 'v1:chip',
32
+ rendering: 'v1:chip-done',
33
+ ...over,
34
+ });
35
+ export const SUITE = [
36
+ {
37
+ subject: 'story:ds-chip--done',
38
+ instances: [
39
+ instance({ component: 'Story', path: '0', depth: 0, renders: ['Chip'] }),
40
+ chip('0/0', 'Story'),
41
+ ],
42
+ },
43
+ {
44
+ subject: 'story:page--default',
45
+ instances: [
46
+ instance({ component: 'App', path: '0', depth: 0, renders: ['Footer'] }),
47
+ instance({ component: 'Footer', path: '0/1', depth: 1, renders: ['Chip', 'Chip'] }),
48
+ chip('0/1/0', 'Footer', { props: 'v1:chip-all', rendering: 'v1:chip-all' }),
49
+ chip('0/1/1', 'Footer'),
50
+ ],
51
+ },
52
+ ];
53
+ //# sourceMappingURL=composition-fixture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composition-fixture.js","sourceRoot":"","sources":["../../src/attribute/composition-fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,MAAM,UAAU,QAAQ,CAAC,IAAwD;IAC/E,OAAO;QACL,IAAI,EAAE,GAAG;QACT,KAAK,EAAE,CAAC;QACR,SAAS,EAAE,MAAM;QACjB,SAAS,EAAE,MAAM;QACjB,SAAS,EAAE,MAAM;QACjB,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,MAAM;QACb,OAAO,EAAE,EAAE;QACX,KAAK,EAAE,CAAC;QACR,MAAM,EAAE,EAAE;QACV,GAAG,IAAI;KACR,CAAC;AACJ,CAAC;AAED,2DAA2D;AAC3D,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,MAAc,EAAE,IAAI,GAA+B,EAAE,EAAE,EAAE,CAC1F,QAAQ,CAAC;IACP,SAAS,EAAE,MAAM;IACjB,IAAI;IACJ,MAAM;IACN,KAAK,EAAE,CAAC;IACR,KAAK,EAAE,SAAS;IAChB,SAAS,EAAE,cAAc;IACzB,GAAG,IAAI;CACR,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,KAAK,GAAkC;IAClD;QACE,OAAO,EAAE,qBAAqB;QAC9B,SAAS,EAAE;YACT,QAAQ,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;YACxE,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC;SACrB;KACF;IACD;QACE,OAAO,EAAE,qBAAqB;QAC9B,SAAS,EAAE;YACT,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxE,QAAQ,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;YACnF,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC;YAC3E,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC;SACxB;KACF;CACF,CAAC","sourcesContent":["/**\n * One small suite, built twice over: as a graph, and as something that moved.\n *\n * `composition.test.ts` proves the fold and `movement.test.ts` proves the ladder\n * that reads it, and they need the same three subjects to do it. A chip in its\n * own story and the same chip in a page is the smallest arrangement in which an\n * echo, a divergence and a control group are all available at once, so it is\n * described once here rather than diverging in two files.\n */\n\nimport type { ComponentInstance } from './instances.js';\nimport type { SubjectComposition } from './composition.js';\n\nexport function instance(over: Partial<ComponentInstance> & { component: string }): ComponentInstance {\n return {\n path: '0',\n depth: 1,\n rendering: 'v1:r',\n structure: 'v1:s',\n semantics: 'v1:a',\n text: 'v1:t',\n style: 'v1:y',\n renders: [],\n nodes: 1,\n tokens: [],\n ...over,\n };\n}\n\n/** A chip, rendered the same way, with the same inputs. */\nexport const chip = (path: string, within: string, over: Partial<ComponentInstance> = {}) =>\n instance({\n component: 'Chip',\n path,\n within,\n depth: 2,\n props: 'v1:chip',\n rendering: 'v1:chip-done',\n ...over,\n });\n\nexport const SUITE: readonly SubjectComposition[] = [\n {\n subject: 'story:ds-chip--done',\n instances: [\n instance({ component: 'Story', path: '0', depth: 0, renders: ['Chip'] }),\n chip('0/0', 'Story'),\n ],\n },\n {\n subject: 'story:page--default',\n instances: [\n instance({ component: 'App', path: '0', depth: 0, renders: ['Footer'] }),\n instance({ component: 'Footer', path: '0/1', depth: 1, renders: ['Chip', 'Chip'] }),\n chip('0/1/0', 'Footer', { props: 'v1:chip-all', rendering: 'v1:chip-all' }),\n chip('0/1/1', 'Footer'),\n ],\n },\n];\n"]}
@@ -0,0 +1,182 @@
1
+ import type { Digest } from '../format/hash.js';
2
+ import type { NodePath, SemanticSnapshot } from '../format/snapshot.js';
3
+ import { type Divergence } from './divergence.js';
4
+ import { type ComponentInstance } from './instances.js';
5
+ /**
6
+ * The suite read as one graph, at one commit.
7
+ *
8
+ * Every other comparison in this system is between two revisions of one subject.
9
+ * This is the other axis: **many subjects, one revision**, joined on the
10
+ * components they share. It answers the question a per-subject tool cannot even
11
+ * phrase — *this component, here, is the same component you already have an
12
+ * example of over there* — and it answers it from digests a run already
13
+ * computed, with no second render, no image and no store.
14
+ *
15
+ * ## What the join is worth
16
+ *
17
+ * A suite of examples is a suite of *compositions*. The narrow example is a
18
+ * component at a boundary; the page examples contain that same boundary again,
19
+ * sometimes with the same props and sometimes not. Once the instances are
20
+ * addressable, three facts fall out that nobody had:
21
+ *
22
+ * - **An echo.** One rendering, several subjects. The narrow example and the
23
+ * page are watching literally the same bytes, so a change in one is a change
24
+ * in all of them, and a reviewer looking at eleven diffs is looking at one.
25
+ * - **A divergence.** Same component, same props, *different* rendering, at one
26
+ * commit. Nothing about the component's own inputs can explain that. Either
27
+ * something outside it decides its output — an ancestor's cascade, a token, its
28
+ * own state — or the reading is not repeatable. Both are findings and neither
29
+ * is a regression.
30
+ * - **A control group.** For any instance that moved, the same rendering
31
+ * somewhere else that did *not* is a stable state to refer to. That is the
32
+ * referent flake attribution has always needed and never had, and the suite
33
+ * supplies it for free: one commit already holds N renderings of every shared
34
+ * component.
35
+ *
36
+ * ## What it deliberately does not do
37
+ *
38
+ * It never decides anything. Two subjects sharing a rendering is not a reason to
39
+ * delete either — that is the argument `coverage.ts` makes at the level of names
40
+ * and it holds harder here, because a component can be correct in one context
41
+ * and broken in the next, which is why the contexts are separate subjects.
42
+ */
43
+ /** One instance, located. */
44
+ export interface Site {
45
+ readonly subject: string;
46
+ readonly path: NodePath;
47
+ readonly depth: number;
48
+ readonly within?: string;
49
+ /**
50
+ * The component whose JSX wrote this element, here.
51
+ *
52
+ * On the entry beside it, `createdBy` is a set folded over the whole suite —
53
+ * every component that ever wrote one of these, anywhere. That is the right
54
+ * shape for *who mounts this* and the wrong one for *who mounted this here*,
55
+ * and the ladder asks the second: an edit to a component that writes a `Chip`
56
+ * on the footer explains nothing about a `Chip` the sidebar wrote. Kept per
57
+ * site so the question can be asked where it was answered.
58
+ */
59
+ readonly createdBy?: string;
60
+ }
61
+ /** One rendering of one component, and everywhere it occurred. */
62
+ export interface Rendering {
63
+ readonly rendering: Digest;
64
+ readonly structure: Digest;
65
+ readonly semantics: Digest;
66
+ readonly text: Digest;
67
+ readonly style: Digest;
68
+ /**
69
+ * Child components this rendering mounted, in document order.
70
+ *
71
+ * Carried on the rendering rather than only on the entry because it is the
72
+ * only evidence available about what was passed *in*: `propsDigest` excludes
73
+ * `children` by design, so two renderings that mount different children are
74
+ * two different inputs wearing one props digest. `divergencesOf` refuses to
75
+ * call that a contradiction, and this is the field it reads.
76
+ */
77
+ readonly renders: readonly string[];
78
+ /** In subject order, then document order. */
79
+ readonly sites: readonly Site[];
80
+ }
81
+ /** One props digest of one component, and every way it rendered under it. */
82
+ export interface PropsClass {
83
+ /** Absent when the collector supplied no provenance — unknown, not "none". */
84
+ readonly props?: Digest;
85
+ readonly renderings: readonly Rendering[];
86
+ }
87
+ export interface ComponentEntry {
88
+ readonly component: string;
89
+ /** Subjects containing at least one boundary of it, in the order supplied. */
90
+ readonly subjects: readonly string[];
91
+ /** Boundaries summed across every subject. Distinct from `subjects.length`. */
92
+ readonly instances: number;
93
+ /**
94
+ * Subjects whose shallowest attributed boundary is this component.
95
+ *
96
+ * The narrow example — the subject that exists to show this thing, rather than
97
+ * a page that happens to contain it. Defined by depth rather than by a naming
98
+ * convention because a naming convention is a different tool's `story:` prefix,
99
+ * and this has to hold for a route suite too. Ties resolve to nothing: a
100
+ * subject whose two shallowest boundaries are siblings has no single subject.
101
+ */
102
+ readonly examples: readonly string[];
103
+ /** Components that enclose it somewhere, sorted. The graph, upwards. */
104
+ readonly within: readonly string[];
105
+ /**
106
+ * Components that *mounted* it somewhere, sorted. The graph, upwards, again —
107
+ * and usually the more useful of the two.
108
+ *
109
+ * `within` is where the boundary sits in the DOM and this is who wrote the
110
+ * element. They are the same thing only for a component that authors a node of
111
+ * its own, and a real application is full of components that do not: measured
112
+ * on `examples/todomvc`, every `Chip` reports `within: ['Stack']` and
113
+ * `createdBy: ['TodoFooter']`. `TodoFooter` renders nothing but other
114
+ * components, so it owns no node, is a boundary nowhere, and appears in this
115
+ * graph *only* here — while being the file a reviewer has to open.
116
+ *
117
+ * Empty on a production build, where `_debugOwner` is absent (ADR-0007). Empty
118
+ * is *not* "nothing mounted it": a component with no caller is the subject root
119
+ * and every other case is missing data, which is why the `upstream` rung in
120
+ * `movement.ts` reads this and never concludes from its silence.
121
+ */
122
+ readonly createdBy: readonly string[];
123
+ /** Components it encloses somewhere, sorted. The graph, downwards. */
124
+ readonly renders: readonly string[];
125
+ /**
126
+ * Custom properties its own nodes resolve through, anywhere in the suite.
127
+ *
128
+ * The set a token movement is intersected against, which is how "`--va-space-3`
129
+ * moved" becomes "`--va-space-3` moved and these four components read it".
130
+ */
131
+ readonly tokens: readonly string[];
132
+ /** Sorted by props digest; the class with unknown props sorts last. */
133
+ readonly classes: readonly PropsClass[];
134
+ }
135
+ /**
136
+ * The same component, the same props, the same bytes, in more than one subject.
137
+ *
138
+ * The connected dots. `example` names the narrow subject among the sites when
139
+ * there is one, because that is the artefact a reviewer already has an opinion
140
+ * about — "this is the Chip from the Chip story" is a sentence, and "these four
141
+ * paths share a digest" is not.
142
+ */
143
+ export interface Echo {
144
+ readonly component: string;
145
+ readonly props?: Digest;
146
+ readonly rendering: Digest;
147
+ /** At least two, spanning at least two subjects. */
148
+ readonly sites: readonly Site[];
149
+ readonly example?: string;
150
+ }
151
+ export interface SubjectComposition {
152
+ readonly subject: string;
153
+ readonly instances: readonly ComponentInstance[];
154
+ /**
155
+ * The subject as read, kept so a divergence can say *which input* moved.
156
+ *
157
+ * Optional because `composeSubjects` is a fold over instance lists and stays
158
+ * one — a caller holding only a report's sidecars still gets the graph, the
159
+ * echoes and the divergences, and gets them without a `partings` field it
160
+ * would have to explain away as empty.
161
+ */
162
+ readonly snapshot?: SemanticSnapshot;
163
+ }
164
+ export interface Composition {
165
+ /** In the order supplied, which the run guarantees is plan order. */
166
+ readonly subjects: readonly string[];
167
+ /** Sorted by name, code-unit order. */
168
+ readonly components: readonly ComponentEntry[];
169
+ /** Sorted by how many sites each has, widest first, then by component. */
170
+ readonly echoes: readonly Echo[];
171
+ /** Sorted by component name. */
172
+ readonly divergences: readonly Divergence[];
173
+ }
174
+ /**
175
+ * Fold a run's per-subject instance lists into the graph.
176
+ *
177
+ * Pure, ordered, and a function of its input alone — the run's report has to be
178
+ * a function of the plan, and a phase that accumulated as a worker pool finished
179
+ * would produce a different artefact from the same suite on a slower machine.
180
+ */
181
+ export declare function composeSubjects(subjects: readonly SubjectComposition[]): Composition;
182
+ //# sourceMappingURL=composition.d.ts.map