@variance-authority/core 0.14.0 → 0.17.1

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 (147) hide show
  1. package/CHANGELOG.md +878 -0
  2. package/README.md +38 -17
  3. package/dist/attribute/component-hash.d.ts +14 -5
  4. package/dist/attribute/component-hash.js +14 -5
  5. package/dist/attribute/component-hash.js.map +1 -1
  6. package/dist/attribute/composition.d.ts +16 -0
  7. package/dist/attribute/composition.js +4 -6
  8. package/dist/attribute/composition.js.map +1 -1
  9. package/dist/attribute/index.d.ts +3 -1
  10. package/dist/attribute/index.js +2 -1
  11. package/dist/attribute/index.js.map +1 -1
  12. package/dist/attribute/movement.d.ts +1 -1
  13. package/dist/attribute/movement.js.map +1 -1
  14. package/dist/attribute/region.d.ts +47 -4
  15. package/dist/attribute/region.js +91 -22
  16. package/dist/attribute/region.js.map +1 -1
  17. package/dist/attribute/source.d.ts +10 -24
  18. package/dist/attribute/source.js +4 -38
  19. package/dist/attribute/source.js.map +1 -1
  20. package/dist/attribute/subject-pieces.d.ts +37 -0
  21. package/dist/attribute/subject-pieces.js +122 -0
  22. package/dist/attribute/subject-pieces.js.map +1 -0
  23. package/dist/compare/band.d.ts +8 -2
  24. package/dist/compare/band.js +7 -2
  25. package/dist/compare/band.js.map +1 -1
  26. package/dist/compare/diff/attribution.js +6 -4
  27. package/dist/compare/diff/attribution.js.map +1 -1
  28. package/dist/compare/diff/compare-nodes.js +9 -1
  29. package/dist/compare/diff/compare-nodes.js.map +1 -1
  30. package/dist/compare/diff/match.d.ts +2 -1
  31. package/dist/compare/diff/match.js +52 -0
  32. package/dist/compare/diff/match.js.map +1 -1
  33. package/dist/compare/parting.js +6 -4
  34. package/dist/compare/parting.js.map +1 -1
  35. package/dist/format/capture.d.ts +25 -0
  36. package/dist/format/capture.js +6 -0
  37. package/dist/format/capture.js.map +1 -1
  38. package/dist/format/document.d.ts +28 -5
  39. package/dist/format/document.js +8 -2
  40. package/dist/format/document.js.map +1 -1
  41. package/dist/format/drive.d.ts +191 -0
  42. package/dist/format/drive.js +343 -0
  43. package/dist/format/drive.js.map +1 -0
  44. package/dist/format/environment.d.ts +20 -2
  45. package/dist/format/environment.js +7 -0
  46. package/dist/format/environment.js.map +1 -1
  47. package/dist/format/index.d.ts +13 -3
  48. package/dist/format/index.js +8 -2
  49. package/dist/format/index.js.map +1 -1
  50. package/dist/format/intervention.d.ts +79 -0
  51. package/dist/format/intervention.js.map +1 -1
  52. package/dist/format/origin.d.ts +32 -0
  53. package/dist/format/origin.js +54 -0
  54. package/dist/format/origin.js.map +1 -0
  55. package/dist/format/packed.d.ts +32 -0
  56. package/dist/format/packed.js +23 -0
  57. package/dist/format/packed.js.map +1 -0
  58. package/dist/format/shape.d.ts +31 -0
  59. package/dist/format/shape.js +269 -0
  60. package/dist/format/shape.js.map +1 -0
  61. package/dist/format/snapshot.d.ts +2 -0
  62. package/dist/format/snapshot.js.map +1 -1
  63. package/dist/format/stabilize.d.ts +35 -6
  64. package/dist/format/stabilize.js +58 -103
  65. package/dist/format/stabilize.js.map +1 -1
  66. package/dist/format/steps.d.ts +44 -0
  67. package/dist/format/steps.js +67 -0
  68. package/dist/format/steps.js.map +1 -0
  69. package/dist/format/waits.d.ts +12 -0
  70. package/dist/format/waits.js +156 -0
  71. package/dist/format/waits.js.map +1 -0
  72. package/dist/format/width.d.ts +35 -0
  73. package/dist/format/width.js +38 -0
  74. package/dist/format/width.js.map +1 -0
  75. package/dist/index.d.ts +2 -1
  76. package/dist/index.js +2 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/judge/docket.js +1 -0
  79. package/dist/judge/docket.js.map +1 -1
  80. package/dist/judge/index.d.ts +1 -0
  81. package/dist/judge/index.js +1 -0
  82. package/dist/judge/index.js.map +1 -1
  83. package/dist/judge/restamp.d.ts +20 -0
  84. package/dist/judge/restamp.js +20 -0
  85. package/dist/judge/restamp.js.map +1 -0
  86. package/dist/nearest.d.ts +46 -0
  87. package/dist/nearest.js +102 -0
  88. package/dist/nearest.js.map +1 -0
  89. package/dist/relate/before.d.ts +19 -24
  90. package/dist/relate/before.js +37 -39
  91. package/dist/relate/before.js.map +1 -1
  92. package/dist/relate/beyond.d.ts +59 -0
  93. package/dist/relate/beyond.js +98 -0
  94. package/dist/relate/beyond.js.map +1 -0
  95. package/dist/relate/carried.d.ts +122 -0
  96. package/dist/relate/carried.js +279 -0
  97. package/dist/relate/carried.js.map +1 -0
  98. package/dist/relate/graph.d.ts +14 -12
  99. package/dist/relate/graph.js +12 -11
  100. package/dist/relate/graph.js.map +1 -1
  101. package/dist/relate/index.d.ts +3 -1
  102. package/dist/relate/index.js +3 -1
  103. package/dist/relate/index.js.map +1 -1
  104. package/dist/relate/merkle.js +2 -11
  105. package/dist/relate/merkle.js.map +1 -1
  106. package/dist/relate/narrow.d.ts +2 -2
  107. package/dist/relate/narrow.js +26 -73
  108. package/dist/relate/narrow.js.map +1 -1
  109. package/dist/relate/reach.d.ts +6 -6
  110. package/dist/relate/reach.js +10 -53
  111. package/dist/relate/reach.js.map +1 -1
  112. package/dist/relate/records.d.ts +6 -5
  113. package/dist/relate/records.js +3 -2
  114. package/dist/relate/records.js.map +1 -1
  115. package/dist/rules/index.d.ts +3 -3
  116. package/dist/rules/index.js +3 -3
  117. package/dist/rules/index.js.map +1 -1
  118. package/dist/rules/normalize/cascade.d.ts +12 -0
  119. package/dist/rules/normalize/cascade.js +34 -16
  120. package/dist/rules/normalize/cascade.js.map +1 -1
  121. package/dist/rules/normalize/fixture.d.ts +3 -0
  122. package/dist/rules/normalize/fixture.js +3 -1
  123. package/dist/rules/normalize/fixture.js.map +1 -1
  124. package/dist/rules/normalize/index.d.ts +2 -2
  125. package/dist/rules/normalize/index.js +9 -2
  126. package/dist/rules/normalize/index.js.map +1 -1
  127. package/dist/rules/normalize/value.d.ts +13 -2
  128. package/dist/rules/normalize/value.js +42 -8
  129. package/dist/rules/normalize/value.js.map +1 -1
  130. package/dist/rules/normalize/wrapper.js +5 -0
  131. package/dist/rules/normalize/wrapper.js.map +1 -1
  132. package/dist/rules/ruleset.d.ts +24 -4
  133. package/dist/rules/ruleset.js +24 -4
  134. package/dist/rules/ruleset.js.map +1 -1
  135. package/dist/segment/index.d.ts +20 -4
  136. package/dist/segment/index.js +28 -7
  137. package/dist/segment/index.js.map +1 -1
  138. package/dist/shard/index.d.ts +11 -0
  139. package/dist/shard/index.js +11 -0
  140. package/dist/shard/index.js.map +1 -0
  141. package/dist/shard/placement.d.ts +119 -0
  142. package/dist/shard/placement.js +156 -0
  143. package/dist/shard/placement.js.map +1 -0
  144. package/dist/share/index.d.ts +1 -1
  145. package/dist/share/index.js +1 -1
  146. package/dist/share/index.js.map +1 -1
  147. package/package.json +8 -1
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  > The Variance Authority format, rules, comparison, attribution and verdicts. Pure data in, pure data out, no DOM and no I/O.
6
6
 
7
7
  Part of [Variance Authority](https://variance-authority.dev).
8
+ Point your coding agent at [`llms.txt`](https://variance-authority.dev/llms.txt).
8
9
 
9
10
  ## What this is for
10
11
 
@@ -158,12 +159,14 @@ specifier exports only the capture artifact.
158
159
  | `core/relate` | what rests on what: a file graph in adjacency form, the components a change reaches, and a closure digest over each one |
159
160
  | `core/segment` | columnar bytes: named columns, interned strings, and the validation a decode performs before it believes a file |
160
161
  | `core/share` | leaving those bytes where another machine finds them, as the latest of a mainline or a branch |
162
+ | `core/shard` | which shard runs which group of a suite, by recorded time, and how many shards it is worth: `shardCount` weighs each shard's `setup` against the time it saves, within a `budget` or up to a `max`, with `workers` running a shard's groups at once |
161
163
 
162
164
  Five of those are the order an answer travels through: `format`, `rules`,
163
- `compare`, `attribute`, `judge`. `plan` and `relate` are asked before anything
165
+ `compare`, `attribute`, `judge`. `plan` and `relate` run before anything
164
166
  is captured — one decides which baselines a run can use, the other which
165
167
  subjects are worth reading. `segment` and `share` are how a derived answer
166
- is written down and handed to another machine.
168
+ is written down and handed to another machine. `shard` splits one run across
169
+ several.
167
170
 
168
171
  ## The four words the output uses
169
172
 
@@ -172,9 +175,9 @@ structure and declared style; `chromium` adds layout and pixels. A profile that
172
175
  cannot measure a box cannot decide pixels either.
173
176
 
174
177
  An **identity** is the content hash that addresses a result, derived from the
175
- whole plan — ruleset version, viewport, policy, interventions. Two runs are
176
- comparable only when their identities match, so changing any part of the plan
177
- changes which baselines the run can see.
178
+ whole plan — ruleset version, viewport, policy, interventions. Two runs share
179
+ a result only when their identities match, so changing any part of the plan
180
+ changes the address its result is stored under.
178
181
 
179
182
  A **band** is the kind of change a delta is, and which band a delta lands in
180
183
  decides how loudly it is reported. Change frequency and change importance run
@@ -185,13 +188,13 @@ are five, loudest first:
185
188
  | band | what changed | example delta kinds |
186
189
  |---|---|---|
187
190
  | `a11y` | a role, accessible name, description or ARIA state | `role-changed`, `name-changed`, `state-changed` |
188
- | `geometry` | boxes appeared, vanished, moved or resized | `node-added`, `node-moved`, `rect-changed`, `attribute-changed` |
189
- | `token` | style values changed while structure stayed the same | `style-changed`, `token-changed` |
191
+ | `geometry` | boxes appeared, vanished, moved, resized or were replaced | `node-added`, `node-moved`, `tag-changed`, `rect-changed`, `attribute-changed` |
192
+ | `token` | style values changed while structure stayed the same | `style-changed`, `token-changed`, except on a pair across two tags, where they are `geometry` |
190
193
  | `content` | text changed and nothing else did | `text-changed` |
191
194
  | `texture` | sub-semantic rendering variance | `raster-residue` |
192
195
 
193
- `bandOf(kind)` performs that assignment and is the default rather than the
194
- answer — policy may promote or demote per project, subject, region or
196
+ `bandOf(kind, { retagged })` performs that assignment and is the default
197
+ rather than the answer — policy may promote or demote per project, subject, region or
195
198
  component. `loudestBand(bands)` collapses a set to the loudest one present, and
196
199
  returns `null` for an empty set so that *nothing changed* and *something changed
197
200
  at the quietest band* stay apart.
@@ -229,7 +232,10 @@ const after = normalize(recapture);
229
232
  const diff = diffSnapshots(before, after); // deltas and roots, no verdict
230
233
 
231
234
  const places = isolateRegions(mask, { cell: 8 }); // pixels → regions
232
- const named = attributeRegions(places.regions, after, { scale: 2 });
235
+ const named = attributeRegions(places.regions, after, {
236
+ scale: 2,
237
+ before, // where the pixels it replaced were
238
+ });
233
239
  ```
234
240
 
235
241
  `diffSnapshots` returns `identical`, the flat `deltas`, the `roots` a docket
@@ -243,6 +249,20 @@ runner.
243
249
  screenshot at 1x and every region lands in the top-left quadrant with the wrong
244
250
  component named — a full, plausible, entirely wrong report.
245
251
 
252
+ `before` is required too: the snapshot of the revision the pixels are compared
253
+ against, or `undefined` when nobody kept it. A change that replaces an element
254
+ paints over the old box and the new one, so each node is joined against its own
255
+ box and the box of the node the differ's matcher pairs it with. A stored baseline
256
+ carries that snapshot packed, as its `snapshot`; `unpackSnapshot` in
257
+ `@variance-authority/observe` reads it back.
258
+
259
+ Each box that left is placed by where it was in its own image. A screenshot
260
+ clipped to the subject starts at the subject's root, and `beforeOrigin` is the
261
+ before image's top-left corner in page CSS pixels. It defaults to the before
262
+ snapshot's root, as `origin` defaults to this render's. When you pass `origin`
263
+ and a `before` snapshot, pass `beforeOrigin` too, or the call throws: the page
264
+ can move what you clipped to between the two shots.
265
+
246
266
  Rendered comparisons throw when the two subjects differ, or when their
247
267
  observation profiles differ.
248
268
 
@@ -251,10 +271,11 @@ observation profiles differ.
251
271
  | call | useful controls |
252
272
  |---|---|
253
273
  | `normalize` | `collapseWrappers` removes layout-only wrapper boundaries, `digestText` includes text content, and `sourceRoot` relativizes source locations |
254
- | `isolateRegions` / `attributeRegions` | `cell` and `limit` bound mask work; `origin` and `containment` describe the coordinate origin and how much a node must contain a region |
274
+ | `isolateRegions` / `attributeRegions` | `cell` and `limit` bound mask work; `origin` and `containment` describe the coordinate origin and how much a node must contain a region; `inserted` names the rows the after image added, and a region inside them goes to the tightest box whose rows span it |
255
275
  | `fingerprintOfMask` | `grid` controls the shape sample and `coverage` the minimum occupied share |
256
276
  | `compareLocales` / ignore validation | `slack` permits a declared locale distance; `sites` supplies resolved ignore locations and `now` evaluates expiry |
257
277
  | `buildDocket` / dependency reach | `sampleSize` limits review examples; `through` selects graph edge kinds, `avoid` names nodes a walk never covers, and `shadows` lists per file the modules its run never loads, so `affectedBy` leaves out a file every trail to which crosses one of its own shadows; `depends` adds the install — which package rests on which — so a dependency bump is a seed like any other and affects only the files that import it; `uses` names what each import binds and each re-export republishes, and `moved` lists per seed the exports its change changed, so `affectedBy` covers only the files that import one of them |
278
+ | `carried` | the walk under every reach question, with your own step per importer: `through` and `avoid` as above; `mask` and `via` take arrays you already hold, so a caller that walks again writes into the same marks and trail; `seedsHold` set to `false` lets a seed take back what a cycle carries to it, for a file that reads its own names through an importer |
258
279
  | `beforeReach` | `sensed` names the directories the scan already answers for, and the descent from a declared entry point stops at the first file under one of them rather than dragging the repository's own source in behind the harness |
259
280
  | `sharedClosures` | `floor` drops a shared subtree below a node count |
260
281
  | `lexiconOf` | `examples`, `declaredIn` and `regions` supply what the instances cannot: which components a subject is the example of, the files declaring each component, and the regions its journey covered |
@@ -263,8 +284,8 @@ observation profiles differ.
263
284
 
264
285
  ## Which input changed
265
286
 
266
- `compare` says what changed. `judge` says whether anyone should mind. Between
267
- them, `partingOf` says which input changed.
287
+ `compare` finds what changed. `judge` decides whether anyone should mind.
288
+ Between them, `partingOf` finds which input changed.
268
289
 
269
290
  Given two snapshots with holdings — the inputs a collector recorded at each
270
291
  component boundary — it walks the boundaries for the shallowest one whose inputs
@@ -293,7 +314,7 @@ run that recorded no component provenance gets `unread` instead.
293
314
  What a suite is made of is a fact about a commit rather than about a run.
294
315
  `core/segment` writes such a thing as columns — equal facts encode to equal
295
316
  bytes, which is what lets a transport skip an upload — and `core/share` keeps
296
- the latest of those bytes on a line. A checkout asks *what is mainline now*,
317
+ the latest of those bytes on a line. A checkout looks up *what is mainline now*,
297
318
  and a line answers that: one mainline or one branch, holding the latest entry
298
319
  of each kind — the run report, one record per suite — and the images those
299
320
  entries name, by digest.
@@ -329,7 +350,7 @@ appended to. `headers` is sent on every request, which is where a bucket's
329
350
  `Authorization` or a deployment's token goes. `method` is the verb a write uses
330
351
  — `PUT` for a bucket, `POST` for a deployment that routes on it. `timeoutMs`
331
352
  is how long one request may take, reading its body included, before the store
332
- is `unreachable` with a message that says it timed out; it is 60 000 when you
353
+ is `unreachable` with a message naming the timeout; it is 60 000 when you
333
354
  leave it out. A presigned base needs only the endpoint. `memoryLineCell()`
334
355
  holds a line in this process, for tests and for a run that shares with itself.
335
356
 
@@ -343,7 +364,7 @@ A publish replaces only the entries it carries, so suites from different jobs
343
364
  land on one line. On a mainline, a held entry whose commit descends from yours
344
365
  stays: that is an older run finishing last. A held entry in a newer format stays
345
366
  on any line. A line reports why it missed — `absent`, `newer`, `refused`,
346
- `unreachable` or `unreadable` — because each asks you for a different action.
367
+ `unreachable` or `unreadable` — because each needs a different action from you.
347
368
  Everything a line holds can be derived again, so a miss costs you the
348
369
  derivation. [Sharing an evaluation](https://variance-authority.dev/docs/sharing)
349
370
  is the operator's side of it.
@@ -361,4 +382,4 @@ the comparison is refused rather than reported as a difference in the product.
361
382
 
362
383
  ---
363
384
 
364
- **[@variance-authority/core](https://variance-authority.dev/reference/packages/core)** is part of [Variance Authority](https://variance-authority.dev) — [documentation](https://variance-authority.dev/docs) · MIT
385
+ **[@variance-authority/core](https://variance-authority.dev/reference/packages/core)** is part of [Variance Authority](https://variance-authority.dev) — [documentation](https://variance-authority.dev/docs) · [GitHub](https://github.com/Variance-Authority/variance-authority/tree/main/packages/core) · MIT
@@ -49,7 +49,7 @@ export declare function hashComponents(snapshot: SemanticSnapshot): readonly Com
49
49
  * The arithmetic behind cause-first ranking, and the reason a baseline carries
50
50
  * its component hashes at all. `rankRegions` takes a list of cause names and,
51
51
  * given none, falls back to area — which
52
- * [journal 0013](../../../../docs/context/journal/0013-observability.md)
52
+ * [journal 0013](https://github.com/Variance-Authority/variance-authority/blob/main/docs/context/journal/0013-observability.md)
53
53
  * measured as backwards by 6×, because area measures displacement rather than
54
54
  * cause. This is where that list comes from on a path with no second document.
55
55
  *
@@ -81,12 +81,20 @@ export declare function causesBetween(before: readonly ComponentHash[], after: r
81
81
  * and a boolean cannot serve a route-level test, whose entire request is *tell
82
82
  * me when the page stops assembling and never when it is repainted*.
83
83
  *
84
- * The mapping is exact and it is the same one `bandOf` applies to a delta, which
85
- * is the property that matters: a subject relaxed to `layout` must absorb the
84
+ * The mapping is the same one `bandOf` applies to a delta, but for the one case
85
+ * below, and that is the property that matters: a subject relaxed to `layout` must absorb the
86
86
  * same things whether the run held two documents or two sidecars. Two mappings
87
87
  * would be one drift away from a config key meaning different things on the two
88
88
  * paths, discovered as a regression somebody let through.
89
89
  *
90
+ * One delta the digests cannot band as `bandOf` does: the style of an element
91
+ * its author replaced, which `bandOf` bands `geometry` with the tag change. Two
92
+ * hashes do not say the tag changed under the style, so the `style` digest still
93
+ * reads `token` beside it. The `structure` digest moves with the tag, so both
94
+ * paths report `geometry`, and the one level that asserts `token`, `strict`,
95
+ * asserts `geometry` too: the verdict agrees. What differs is the list a
96
+ * relaxed reason gives as absorbed, which names `token` on this path only.
97
+ *
90
98
  * | digest | band | what it covers |
91
99
  * |---|---|---|
92
100
  * | `semantics` | `a11y` | role, accessible name, ARIA state |
@@ -103,8 +111,9 @@ export declare function causesBetween(before: readonly ComponentHash[], after: r
103
111
  * A missing `geometry` on either side is the profile saying it has no layout
104
112
  * engine, and is not a difference. Treating absent as a change would report
105
113
  * every component as having moved the moment a jsdom baseline met a Chromium
106
- * run — which the environment key already refuses as `incomparable`, so this
107
- * would be a second, wronger answer to a question already settled.
114
+ * run — which the environment key already names as the cause of that
115
+ * comparison, so this would be a second, wronger answer to a question already
116
+ * settled.
108
117
  */
109
118
  export declare function movedBands(before: BandDigests, after: BandDigests): readonly Band[];
110
119
  /**
@@ -92,7 +92,7 @@ function byCodeUnit(a, b) {
92
92
  * The arithmetic behind cause-first ranking, and the reason a baseline carries
93
93
  * its component hashes at all. `rankRegions` takes a list of cause names and,
94
94
  * given none, falls back to area — which
95
- * [journal 0013](../../../../docs/context/journal/0013-observability.md)
95
+ * [journal 0013](https://github.com/Variance-Authority/variance-authority/blob/main/docs/context/journal/0013-observability.md)
96
96
  * measured as backwards by 6×, because area measures displacement rather than
97
97
  * cause. This is where that list comes from on a path with no second document.
98
98
  *
@@ -176,12 +176,20 @@ function ownContentMoved(before, after) {
176
176
  * and a boolean cannot serve a route-level test, whose entire request is *tell
177
177
  * me when the page stops assembling and never when it is repainted*.
178
178
  *
179
- * The mapping is exact and it is the same one `bandOf` applies to a delta, which
180
- * is the property that matters: a subject relaxed to `layout` must absorb the
179
+ * The mapping is the same one `bandOf` applies to a delta, but for the one case
180
+ * below, and that is the property that matters: a subject relaxed to `layout` must absorb the
181
181
  * same things whether the run held two documents or two sidecars. Two mappings
182
182
  * would be one drift away from a config key meaning different things on the two
183
183
  * paths, discovered as a regression somebody let through.
184
184
  *
185
+ * One delta the digests cannot band as `bandOf` does: the style of an element
186
+ * its author replaced, which `bandOf` bands `geometry` with the tag change. Two
187
+ * hashes do not say the tag changed under the style, so the `style` digest still
188
+ * reads `token` beside it. The `structure` digest moves with the tag, so both
189
+ * paths report `geometry`, and the one level that asserts `token`, `strict`,
190
+ * asserts `geometry` too: the verdict agrees. What differs is the list a
191
+ * relaxed reason gives as absorbed, which names `token` on this path only.
192
+ *
185
193
  * | digest | band | what it covers |
186
194
  * |---|---|---|
187
195
  * | `semantics` | `a11y` | role, accessible name, ARIA state |
@@ -198,8 +206,9 @@ function ownContentMoved(before, after) {
198
206
  * A missing `geometry` on either side is the profile saying it has no layout
199
207
  * engine, and is not a difference. Treating absent as a change would report
200
208
  * every component as having moved the moment a jsdom baseline met a Chromium
201
- * run — which the environment key already refuses as `incomparable`, so this
202
- * would be a second, wronger answer to a question already settled.
209
+ * run — which the environment key already names as the cause of that
210
+ * comparison, so this would be a second, wronger answer to a question already
211
+ * settled.
203
212
  */
204
213
  export function movedBands(before, after) {
205
214
  const moved = new Set();
@@ -1 +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 { declaredIn } from '../compare/cascade.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 const declared = declaredIn(snapshot);\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 values: Map<string, Set<string>>;\n }\n >();\n\n for (const boundary of boundaries(snapshot.root)) {\n const shape = shapeOf(boundary, layout, undefined, declared);\n\n const entry = accumulated.get(boundary.component) ?? {\n structure: [],\n semantics: [],\n text: [],\n style: [],\n geometry: [],\n boxes: [],\n values: new Map<string, Set<string>>(),\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 for (const [property, values] of Object.entries(shape.values ?? {})) {\n const seen = entry.values.get(property) ?? new Set<string>();\n for (const value of values) seen.add(value);\n entry.values.set(property, seen);\n }\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 values: sortedValues(entry.values),\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) => byCodeUnit(a.component, b.component));\n}\n\n/** Properties and their values in code-unit order, so the sidecar is byte-stable. */\nfunction sortedValues(values: ReadonlyMap<string, ReadonlySet<string>>): Record<string, string[]> {\n const sorted: Record<string, string[]> = {};\n for (const property of [...values.keys()].sort(byCodeUnit)) {\n sorted[property] = [...values.get(property)!].sort(byCodeUnit);\n }\n return sorted;\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 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 * Which declared values changed, by property, when both sides recorded them.\n *\n * `token` says a declaration under here moved; this names it. `from` holds the\n * values only the baseline had and `to` the values only this run has, so a\n * component rendered as primary and secondary that changed one background\n * reports that background and not the other. An empty side is a property no\n * node declared on that side, which was measured and is not the same as absent.\n *\n * Absent when either sidecar predates the record, and when the style digest\n * moved but no property's set of values did — a value that moved from one\n * node to another is a real change this cannot describe, and an empty list\n * would say there was none. Present is never empty.\n */\n readonly changed?: readonly ValueChange[];\n}\n\n/** One property whose set of declared values differs between two revisions. */\nexport interface ValueChange {\n readonly property: string;\n readonly from: readonly string[];\n readonly to: readonly string[];\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 const changed = was.style === entry.style ? undefined : changedBetween(was.values, entry.values);\n moved.push({\n component: entry.component,\n bands,\n cause: ownContentMoved(was, entry),\n ...(grew === undefined ? {} : { grew }),\n ...(changed === undefined ? {} : { changed }),\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) => byCodeUnit(left.component, right.component));\n}\n\n/**\n * Every property whose values differ, or nothing when that cannot be said.\n *\n * Read only when the `style` digest moved, which is the digest these values\n * were declared into: a component whose declarations hashed the same has\n * nothing here to name, whatever else about it changed.\n */\nfunction changedBetween(\n before: ComponentHash['values'],\n after: ComponentHash['values'],\n): readonly ValueChange[] | undefined {\n if (before === undefined || after === undefined) return undefined;\n\n const changed: ValueChange[] = [];\n const properties = new Set([...Object.keys(before), ...Object.keys(after)]);\n for (const property of [...properties].sort(byCodeUnit)) {\n const was = before[property] ?? [];\n const now = after[property] ?? [];\n const from = was.filter((value) => !now.includes(value));\n const to = now.filter((value) => !was.includes(value));\n if (from.length > 0 || to.length > 0) changed.push({ property, from, to });\n }\n return changed.length === 0 ? undefined : changed;\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"]}
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 { declaredIn } from '../compare/cascade.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 const declared = declaredIn(snapshot);\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 values: Map<string, Set<string>>;\n }\n >();\n\n for (const boundary of boundaries(snapshot.root)) {\n const shape = shapeOf(boundary, layout, undefined, declared);\n\n const entry = accumulated.get(boundary.component) ?? {\n structure: [],\n semantics: [],\n text: [],\n style: [],\n geometry: [],\n boxes: [],\n values: new Map<string, Set<string>>(),\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 for (const [property, values] of Object.entries(shape.values ?? {})) {\n const seen = entry.values.get(property) ?? new Set<string>();\n for (const value of values) seen.add(value);\n entry.values.set(property, seen);\n }\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 values: sortedValues(entry.values),\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) => byCodeUnit(a.component, b.component));\n}\n\n/** Properties and their values in code-unit order, so the sidecar is byte-stable. */\nfunction sortedValues(values: ReadonlyMap<string, ReadonlySet<string>>): Record<string, string[]> {\n const sorted: Record<string, string[]> = {};\n for (const property of [...values.keys()].sort(byCodeUnit)) {\n sorted[property] = [...values.get(property)!].sort(byCodeUnit);\n }\n return sorted;\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 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](https://github.com/Variance-Authority/variance-authority/blob/main/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 the same one `bandOf` applies to a delta, but for the one case\n * below, and that 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 * One delta the digests cannot band as `bandOf` does: the style of an element\n * its author replaced, which `bandOf` bands `geometry` with the tag change. Two\n * hashes do not say the tag changed under the style, so the `style` digest still\n * reads `token` beside it. The `structure` digest moves with the tag, so both\n * paths report `geometry`, and the one level that asserts `token`, `strict`,\n * asserts `geometry` too: the verdict agrees. What differs is the list a\n * relaxed reason gives as absorbed, which names `token` on this path only.\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 names as the cause of that\n * comparison, so this would be a second, wronger answer to a question already\n * 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 * Which declared values changed, by property, when both sides recorded them.\n *\n * `token` says a declaration under here moved; this names it. `from` holds the\n * values only the baseline had and `to` the values only this run has, so a\n * component rendered as primary and secondary that changed one background\n * reports that background and not the other. An empty side is a property no\n * node declared on that side, which was measured and is not the same as absent.\n *\n * Absent when either sidecar predates the record, and when the style digest\n * moved but no property's set of values did — a value that moved from one\n * node to another is a real change this cannot describe, and an empty list\n * would say there was none. Present is never empty.\n */\n readonly changed?: readonly ValueChange[];\n}\n\n/** One property whose set of declared values differs between two revisions. */\nexport interface ValueChange {\n readonly property: string;\n readonly from: readonly string[];\n readonly to: readonly string[];\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 const changed = was.style === entry.style ? undefined : changedBetween(was.values, entry.values);\n moved.push({\n component: entry.component,\n bands,\n cause: ownContentMoved(was, entry),\n ...(grew === undefined ? {} : { grew }),\n ...(changed === undefined ? {} : { changed }),\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) => byCodeUnit(left.component, right.component));\n}\n\n/**\n * Every property whose values differ, or nothing when that cannot be said.\n *\n * Read only when the `style` digest moved, which is the digest these values\n * were declared into: a component whose declarations hashed the same has\n * nothing here to name, whatever else about it changed.\n */\nfunction changedBetween(\n before: ComponentHash['values'],\n after: ComponentHash['values'],\n): readonly ValueChange[] | undefined {\n if (before === undefined || after === undefined) return undefined;\n\n const changed: ValueChange[] = [];\n const properties = new Set([...Object.keys(before), ...Object.keys(after)]);\n for (const property of [...properties].sort(byCodeUnit)) {\n const was = before[property] ?? [];\n const now = after[property] ?? [];\n const from = was.filter((value) => !now.includes(value));\n const to = now.filter((value) => !was.includes(value));\n if (from.length > 0 || to.length > 0) changed.push({ property, from, to });\n }\n return changed.length === 0 ? undefined : changed;\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"]}
@@ -181,4 +181,20 @@ export interface Composition {
181
181
  * would produce a different artefact from the same suite on a slower machine.
182
182
  */
183
183
  export declare function composeSubjects(subjects: readonly SubjectComposition[]): Composition;
184
+ /**
185
+ * Components more than half the suite mounts.
186
+ *
187
+ * Structure, not subject matter. A harness wrapper, a theme or store provider,
188
+ * a portal root and an HOC that every screen is wrapped in are all mounted by
189
+ * nearly every subject, and none of them is what any subject is about. The test
190
+ * is a count over the suite rather than a list of names, which is the only form
191
+ * that survives contact with a real application: nothing here knows that
192
+ * `withStyles(x)` is an HOC, that a minified `aL` is a decorator, or that a
193
+ * class component is a context consumer, and nothing here needs to.
194
+ *
195
+ * More than half, because that is where the claim becomes checkable. A
196
+ * component two thirds of a suite mounts cannot be what distinguishes a subject
197
+ * from its neighbours, whatever it is called.
198
+ */
199
+ export declare function structuralComponents(subjects: readonly SubjectComposition[]): ReadonlySet<string>;
184
200
  //# sourceMappingURL=composition.d.ts.map
@@ -127,14 +127,12 @@ function echoesOf(components) {
127
127
  * component two thirds of a suite mounts cannot be what distinguishes a subject
128
128
  * from its neighbours, whatever it is called.
129
129
  */
130
- function structuralComponents(subjects) {
130
+ export function structuralComponents(subjects) {
131
131
  const mounts = new Map();
132
132
  for (const subject of subjects) {
133
- for (const instance of subject.instances) {
134
- if (!attributed(instance))
135
- continue;
136
- mounts.set(instance.component, (mounts.get(instance.component) ?? 0) + 1);
137
- }
133
+ const mounted = new Set(subject.instances.filter(attributed).map((instance) => instance.component));
134
+ for (const component of mounted)
135
+ mounts.set(component, (mounts.get(component) ?? 0) + 1);
138
136
  }
139
137
  const structural = new Set();
140
138
  for (const [component, held] of mounts) {
@@ -1 +1 @@
1
- {"version":3,"file":"composition.js","sourceRoot":"","sources":["../../src/attribute/composition.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticSnapshot } from '../format/snapshot.js';\nimport { divergencesOf, type Divergence } from './divergence.js';\nimport { attributed, type ComponentInstance } from './instances.js';\n\n/**\n * The suite read as one graph, at one commit.\n *\n * Every other comparison in this system is between two revisions of one subject.\n * This is the other axis: **many subjects, one revision**, joined on the\n * components they share. It answers the question a per-subject tool cannot even\n * phrase — *this component, here, is the same component you already have an\n * example of over there* — and it answers it from digests a run already\n * computed, with no second render, no image and no store.\n *\n * ## What the join is worth\n *\n * A suite of examples is a suite of *compositions*. The narrow example is a\n * component at a boundary; the page examples contain that same boundary again,\n * sometimes with the same props and sometimes not. Once the instances are\n * addressable, three facts fall out that nobody had:\n *\n * - **An echo.** One rendering, several subjects. The narrow example and the\n * page are watching literally the same bytes, so a change in one is a change\n * in all of them, and a reviewer looking at eleven diffs is looking at one.\n * - **A divergence.** Same component, same props, *different* rendering, at one\n * commit. Nothing about the component's own inputs can explain that. Either\n * something outside it decides its output — an ancestor's cascade, a token, its\n * own state — or the reading is not repeatable. Both are findings and neither\n * is a regression.\n * - **A control group.** For any instance that moved, the same rendering\n * somewhere else that did *not* is a stable state to refer to. That is the\n * referent flake attribution has always needed and never had, and the suite\n * supplies it for free: one commit already holds N renderings of every shared\n * component.\n *\n * ## What it deliberately does not do\n *\n * It never decides anything. Two subjects sharing a rendering is not a reason to\n * delete either — that is the argument `coverage.ts` makes at the level of names\n * and it holds harder here, because a component can be correct in one context\n * and broken in the next, which is why the contexts are separate subjects.\n */\n\n/** One instance, located. */\nexport interface Site {\n readonly subject: string;\n readonly path: NodePath;\n readonly depth: number;\n readonly within?: string;\n\n /**\n * The component whose JSX wrote this element, here.\n *\n * On the entry beside it, `createdBy` is a set folded over the whole suite —\n * every component that ever wrote one of these, anywhere. That is the right\n * shape for *who mounts this* and the wrong one for *who mounted this here*,\n * and the ladder asks the second: an edit to a component that writes a `Chip`\n * on the footer explains nothing about a `Chip` the sidebar wrote. Kept per\n * site so the question can be asked where it was answered.\n */\n readonly createdBy?: string;\n}\n\n/** One rendering of one component, and everywhere it occurred. */\nexport interface Rendering {\n readonly rendering: Digest;\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n\n /**\n * Child components this rendering mounted, in document order.\n *\n * Carried on the rendering rather than only on the entry because it is the\n * only evidence available about what was passed *in*: `propsDigest` excludes\n * `children` by design, so two renderings that mount different children are\n * two different inputs wearing one props digest. `divergencesOf` refuses to\n * call that a contradiction, and this is the field it reads.\n */\n readonly renders: readonly string[];\n\n /** In subject order, then document order. */\n readonly sites: readonly Site[];\n}\n\n/** One props digest of one component, and every way it rendered under it. */\nexport interface PropsClass {\n /** Absent when the collector supplied no provenance — unknown, not \"none\". */\n readonly props?: Digest;\n readonly renderings: readonly Rendering[];\n}\n\nexport interface ComponentEntry {\n readonly component: string;\n /** Subjects containing at least one boundary of it, in the order supplied. */\n readonly subjects: readonly string[];\n /** Boundaries summed across every subject. Distinct from `subjects.length`. */\n readonly instances: number;\n\n /**\n * Subjects whose shallowest attributed boundary is this component.\n *\n * The narrow example — the subject that exists to show this thing, rather than\n * a page that happens to contain it. Defined by depth rather than by a naming\n * convention because a naming convention is a different tool's `story:` prefix,\n * and this has to hold for a route suite too. Ties resolve to nothing: a\n * subject whose two shallowest boundaries are siblings has no single subject.\n */\n readonly examples: readonly string[];\n\n /** Components that enclose it somewhere, sorted. The graph, upwards. */\n readonly within: readonly string[];\n\n /**\n * Components that *mounted* it somewhere, sorted. The graph, upwards, again —\n * and usually the more useful of the two.\n *\n * `within` is where the boundary sits in the DOM and this is who wrote the\n * element. They are the same thing only for a component that authors a node of\n * its own, and a real application is full of components that do not: measured\n * on `examples/todomvc`, every `Chip` reports `within: ['Stack']` and\n * `createdBy: ['TodoFooter']`: the `Stack` that encloses a chip is a layout\n * primitive that knows nothing about it, and the footer that wrote the element\n * stands one boundary further out. A component that renders nothing but other\n * components owns no node, is a boundary nowhere, and appears in this graph\n * *only* here — while being the file a reviewer has to open.\n *\n * Empty on a production build, where `_debugOwner` is absent (ADR-0007). Empty\n * is *not* \"nothing mounted it\": a component with no caller is the subject root\n * and every other case is missing data, which is why the `upstream` rung in\n * `movement.ts` reads this and never concludes from its silence.\n */\n readonly createdBy: readonly string[];\n\n /** Components it encloses somewhere, sorted. The graph, downwards. */\n readonly renders: readonly string[];\n\n /**\n * Custom properties its own nodes resolve through, anywhere in the suite.\n *\n * The set a token movement is intersected against, which is how \"`--va-space-3`\n * moved\" becomes \"`--va-space-3` moved and these four components read it\".\n */\n readonly tokens: readonly string[];\n\n /** Sorted by props digest; the class with unknown props sorts last. */\n readonly classes: readonly PropsClass[];\n}\n\n/**\n * The same component, the same props, the same bytes, in more than one subject.\n *\n * The connected dots. `example` names the narrow subject among the sites when\n * there is one, because that is the artefact a reviewer already has an opinion\n * about — \"this is the Chip from the Chip story\" is a sentence, and \"these four\n * paths share a digest\" is not.\n */\nexport interface Echo {\n readonly component: string;\n readonly props?: Digest;\n readonly rendering: Digest;\n /** At least two, spanning at least two subjects. */\n readonly sites: readonly Site[];\n readonly example?: string;\n}\n\n\nexport interface SubjectComposition {\n readonly subject: string;\n readonly instances: readonly ComponentInstance[];\n\n /**\n * The subject as read, kept so a divergence can say *which input* moved.\n *\n * Optional because `composeSubjects` is a fold over instance lists and stays\n * one — a caller holding only a report's sidecars still gets the graph, the\n * echoes and the divergences, and gets them without a `partings` field it\n * would have to explain away as empty.\n */\n readonly snapshot?: SemanticSnapshot;\n}\n\nexport interface Composition {\n /** In the order supplied, which the run guarantees is plan order. */\n readonly subjects: readonly string[];\n /** Sorted by name, code-unit order. */\n readonly components: readonly ComponentEntry[];\n /** Sorted by how many sites each has, widest first, then by component. */\n readonly echoes: readonly Echo[];\n /** Sorted by component name. */\n readonly divergences: readonly Divergence[];\n}\n\n/**\n * Fold a run's per-subject instance lists into the graph.\n *\n * Pure, ordered, and a function of its input alone — the run's report has to be\n * a function of the plan, and a phase that accumulated as a worker pool finished\n * would produce a different artefact from the same suite on a slower machine.\n */\nexport function composeSubjects(subjects: readonly SubjectComposition[]): Composition {\n const entries = new Map<string, Accumulator>();\n const order = subjects.map((subject) => subject.subject);\n const structural = structuralComponents(subjects);\n\n for (const subject of subjects) {\n const example = exampleOf(subject.instances, structural);\n\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n\n const entry = accumulate(entries, instance.component);\n if (entry.subjects.at(-1) !== subject.subject) entry.subjects.push(subject.subject);\n entry.instances += 1;\n if (example === instance.component && entry.examples.at(-1) !== subject.subject) {\n entry.examples.push(subject.subject);\n }\n if (instance.within !== undefined) entry.within.add(instance.within);\n if (instance.createdBy !== undefined) entry.createdBy.add(instance.createdBy);\n for (const child of instance.renders) entry.renders.add(child);\n for (const token of instance.tokens) entry.tokens.add(token);\n\n const key = instance.props ?? UNKNOWN_PROPS;\n const byProps = entry.classes.get(key) ?? new Map<Digest, MutableRendering>();\n const rendering = byProps.get(instance.rendering) ?? {\n rendering: instance.rendering,\n structure: instance.structure,\n semantics: instance.semantics,\n text: instance.text,\n style: instance.style,\n renders: instance.renders,\n sites: [],\n };\n rendering.sites.push({\n subject: subject.subject,\n path: instance.path,\n depth: instance.depth,\n ...(instance.within === undefined ? {} : { within: instance.within }),\n ...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),\n });\n byProps.set(instance.rendering, rendering);\n entry.classes.set(key, byProps);\n }\n }\n\n const components = [...entries.entries()]\n .map(([component, entry]) => ({\n component,\n subjects: entry.subjects,\n instances: entry.instances,\n examples: entry.examples,\n within: [...entry.within].sort(byCodeUnit),\n createdBy: [...entry.createdBy].sort(byCodeUnit),\n renders: [...entry.renders].sort(byCodeUnit),\n tokens: [...entry.tokens].sort(byCodeUnit),\n classes: [...entry.classes.entries()]\n .sort(([a], [b]) => byCodeUnit(a, b))\n .map(([props, renderings]) => ({\n ...(props === UNKNOWN_PROPS ? {} : { props }),\n renderings: [...renderings.values()].sort(widestFirst),\n })),\n }))\n .sort((a, b) => byCodeUnit(a.component, b.component));\n\n const snapshots = new Map<string, SemanticSnapshot>();\n for (const subject of subjects) {\n if (subject.snapshot !== undefined) snapshots.set(subject.subject, subject.snapshot);\n }\n\n return {\n subjects: order,\n components,\n echoes: echoesOf(components),\n divergences: divergencesOf(components, snapshots),\n };\n}\n\n/**\n * Every rendering shared by two or more subjects.\n *\n * Two instances of one component inside a *single* subject sharing a rendering\n * is ordinary and says nothing — a list of three identical chips is three\n * identical chips. The finding is a rendering that survives being mounted\n * somewhere else, because that is the one a second subject is watching.\n */\nfunction echoesOf(components: readonly ComponentEntry[]): readonly Echo[] {\n const echoes: Echo[] = [];\n\n for (const entry of components) {\n for (const group of entry.classes) {\n for (const rendering of group.renderings) {\n const spread = new Set(rendering.sites.map((site) => site.subject));\n if (spread.size < 2) continue;\n\n const example = entry.examples.find((subject) => spread.has(subject));\n echoes.push({\n component: entry.component,\n ...(group.props === undefined ? {} : { props: group.props }),\n rendering: rendering.rendering,\n sites: rendering.sites,\n ...(example === undefined ? {} : { example }),\n });\n }\n }\n }\n\n return echoes.sort(\n (a, b) => b.sites.length - a.sites.length || byCodeUnit(a.component, b.component),\n );\n}\n\n\n/**\n * Components more than half the suite mounts.\n *\n * Structure, not subject matter. A harness wrapper, a theme or store provider,\n * a portal root and an HOC that every screen is wrapped in are all mounted by\n * nearly every subject, and none of them is what any subject is about. The test\n * is a count over the suite rather than a list of names, which is the only form\n * that survives contact with a real application: nothing here knows that\n * `withStyles(x)` is an HOC, that a minified `aL` is a decorator, or that a\n * class component is a context consumer, and nothing here needs to.\n *\n * More than half, because that is where the claim becomes checkable. A\n * component two thirds of a suite mounts cannot be what distinguishes a subject\n * from its neighbours, whatever it is called.\n */\nfunction structuralComponents(subjects: readonly SubjectComposition[]): ReadonlySet<string> {\n const mounts = new Map<string, number>();\n for (const subject of subjects) {\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n mounts.set(instance.component, (mounts.get(instance.component) ?? 0) + 1);\n }\n }\n\n const structural = new Set<string>();\n for (const [component, held] of mounts) {\n if (held * 2 > subjects.length) structural.add(component);\n }\n return structural;\n}\n\n/**\n * The component a subject exists to show, when one component does.\n *\n * The shallowest attributed boundary that is not structure, and only when it is\n * alone at its depth. A story usually mounts one thing; a page mounts a layout\n * that mounts several, and calling the first of them the subject's component\n * would be picking a winner out of document order.\n *\n * The descent is the whole of the difference between this and the obvious rule.\n * Taking the shallowest boundary outright answers with whatever the harness\n * mounted first, which on a real suite is one value for every subject in it —\n * `Wrapper` across a component library, a minified decorator root across a\n * built Storybook — and a field with one value is a field that says nothing. A\n * depth every one of whose names is structure is passed through however many\n * names it has, because a harness that mounts a provider beside a portal root\n * has still not said what the subject is about.\n *\n * Where the descent finds nothing it answers as the shallowest rule would, so\n * this can name more subjects than that rule and never fewer.\n */\nfunction exampleOf(\n instances: readonly ComponentInstance[],\n structural: ReadonlySet<string>,\n): string | undefined {\n const held = instances.filter(attributed);\n const depths = [...new Set(held.map((instance) => instance.depth))].sort((a, b) => a - b);\n\n for (const depth of depths) {\n const names = new Set(held.filter((instance) => instance.depth === depth).map((i) => i.component));\n const matter = [...names].filter((name) => !structural.has(name));\n if (matter.length === 0) continue;\n return matter.length === 1 ? matter[0] : shallowestOf(held, depths[0]);\n }\n\n return shallowestOf(held, depths[0]);\n}\n\n/** The shallowest boundary when it is alone at its depth: the rule before the descent. */\nfunction shallowestOf(held: readonly ComponentInstance[], shallowest: number | undefined): string | undefined {\n if (shallowest === undefined) return undefined;\n const leads = held.filter((instance) => instance.depth === shallowest);\n const names = new Set(leads.map((instance) => instance.component));\n return names.size === 1 ? leads[0]?.component : undefined;\n}\n\n/**\n * The key an instance with no props digest is filed under.\n *\n * A sentinel rather than `undefined`, because a `Map` keyed on `Digest |\n * undefined` reads as though unknown props were a props class like any other,\n * and every consumer would then have to remember that it is not. It sorts last\n * by construction — no digest starts with a space.\n */\nconst UNKNOWN_PROPS = ' unknown';\n\ninterface Accumulator {\n readonly subjects: string[];\n instances: number;\n readonly examples: string[];\n readonly within: Set<string>;\n readonly createdBy: Set<string>;\n readonly renders: Set<string>;\n readonly tokens: Set<string>;\n readonly classes: Map<string, Map<Digest, MutableRendering>>;\n}\n\n/** A `Rendering` while its sites are still being collected. */\ninterface MutableRendering extends Omit<Rendering, 'sites'> {\n readonly sites: Site[];\n}\n\nfunction accumulate(entries: Map<string, Accumulator>, component: string): Accumulator {\n const existing = entries.get(component);\n if (existing !== undefined) return existing;\n\n const created: Accumulator = {\n subjects: [],\n instances: 0,\n examples: [],\n within: new Set(),\n createdBy: new Set(),\n renders: new Set(),\n tokens: new Set(),\n classes: new Map(),\n };\n entries.set(component, created);\n return created;\n}\n\nfunction widestFirst(a: Rendering, b: Rendering): number {\n return b.sites.length - a.sites.length || byCodeUnit(a.rendering, b.rendering);\n}\n\n/**\n * Code-unit order, never `localeCompare`.\n *\n * The same rule `hashComponents` sorts by, for the same reason: this output\n * reaches a report that is committed, diffed and read back on another runner,\n * and a locale-aware comparison makes the byte order a promise about `LANG`.\n */\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
1
+ {"version":3,"file":"composition.js","sourceRoot":"","sources":["../../src/attribute/composition.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticSnapshot } from '../format/snapshot.js';\nimport { divergencesOf, type Divergence } from './divergence.js';\nimport { attributed, type ComponentInstance } from './instances.js';\n\n/**\n * The suite read as one graph, at one commit.\n *\n * Every other comparison in this system is between two revisions of one subject.\n * This is the other axis: **many subjects, one revision**, joined on the\n * components they share. It answers the question a per-subject tool cannot even\n * phrase — *this component, here, is the same component you already have an\n * example of over there* — and it answers it from digests a run already\n * computed, with no second render, no image and no store.\n *\n * ## What the join is worth\n *\n * A suite of examples is a suite of *compositions*. The narrow example is a\n * component at a boundary; the page examples contain that same boundary again,\n * sometimes with the same props and sometimes not. Once the instances are\n * addressable, three facts fall out that nobody had:\n *\n * - **An echo.** One rendering, several subjects. The narrow example and the\n * page are watching literally the same bytes, so a change in one is a change\n * in all of them, and a reviewer looking at eleven diffs is looking at one.\n * - **A divergence.** Same component, same props, *different* rendering, at one\n * commit. Nothing about the component's own inputs can explain that. Either\n * something outside it decides its output — an ancestor's cascade, a token, its\n * own state — or the reading is not repeatable. Both are findings and neither\n * is a regression.\n * - **A control group.** For any instance that moved, the same rendering\n * somewhere else that did *not* is a stable state to refer to. That is the\n * referent flake attribution has always needed and never had, and the suite\n * supplies it for free: one commit already holds N renderings of every shared\n * component.\n *\n * ## What it deliberately does not do\n *\n * It never decides anything. Two subjects sharing a rendering is not a reason to\n * delete either — that is the argument `coverage.ts` makes at the level of names\n * and it holds harder here, because a component can be correct in one context\n * and broken in the next, which is why the contexts are separate subjects.\n */\n\n/** One instance, located. */\nexport interface Site {\n readonly subject: string;\n readonly path: NodePath;\n readonly depth: number;\n readonly within?: string;\n\n /**\n * The component whose JSX wrote this element, here.\n *\n * On the entry beside it, `createdBy` is a set folded over the whole suite —\n * every component that ever wrote one of these, anywhere. That is the right\n * shape for *who mounts this* and the wrong one for *who mounted this here*,\n * and the ladder asks the second: an edit to a component that writes a `Chip`\n * on the footer explains nothing about a `Chip` the sidebar wrote. Kept per\n * site so the question can be asked where it was answered.\n */\n readonly createdBy?: string;\n}\n\n/** One rendering of one component, and everywhere it occurred. */\nexport interface Rendering {\n readonly rendering: Digest;\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n\n /**\n * Child components this rendering mounted, in document order.\n *\n * Carried on the rendering rather than only on the entry because it is the\n * only evidence available about what was passed *in*: `propsDigest` excludes\n * `children` by design, so two renderings that mount different children are\n * two different inputs wearing one props digest. `divergencesOf` refuses to\n * call that a contradiction, and this is the field it reads.\n */\n readonly renders: readonly string[];\n\n /** In subject order, then document order. */\n readonly sites: readonly Site[];\n}\n\n/** One props digest of one component, and every way it rendered under it. */\nexport interface PropsClass {\n /** Absent when the collector supplied no provenance — unknown, not \"none\". */\n readonly props?: Digest;\n readonly renderings: readonly Rendering[];\n}\n\nexport interface ComponentEntry {\n readonly component: string;\n /** Subjects containing at least one boundary of it, in the order supplied. */\n readonly subjects: readonly string[];\n /** Boundaries summed across every subject. Distinct from `subjects.length`. */\n readonly instances: number;\n\n /**\n * Subjects whose shallowest attributed boundary is this component.\n *\n * The narrow example — the subject that exists to show this thing, rather than\n * a page that happens to contain it. Defined by depth rather than by a naming\n * convention because a naming convention is a different tool's `story:` prefix,\n * and this has to hold for a route suite too. Ties resolve to nothing: a\n * subject whose two shallowest boundaries are siblings has no single subject.\n */\n readonly examples: readonly string[];\n\n /** Components that enclose it somewhere, sorted. The graph, upwards. */\n readonly within: readonly string[];\n\n /**\n * Components that *mounted* it somewhere, sorted. The graph, upwards, again —\n * and usually the more useful of the two.\n *\n * `within` is where the boundary sits in the DOM and this is who wrote the\n * element. They are the same thing only for a component that authors a node of\n * its own, and a real application is full of components that do not: measured\n * on `examples/todomvc`, every `Chip` reports `within: ['Stack']` and\n * `createdBy: ['TodoFooter']`: the `Stack` that encloses a chip is a layout\n * primitive that knows nothing about it, and the footer that wrote the element\n * stands one boundary further out. A component that renders nothing but other\n * components owns no node, is a boundary nowhere, and appears in this graph\n * *only* here — while being the file a reviewer has to open.\n *\n * Empty on a production build, where `_debugOwner` is absent (ADR-0007). Empty\n * is *not* \"nothing mounted it\": a component with no caller is the subject root\n * and every other case is missing data, which is why the `upstream` rung in\n * `movement.ts` reads this and never concludes from its silence.\n */\n readonly createdBy: readonly string[];\n\n /** Components it encloses somewhere, sorted. The graph, downwards. */\n readonly renders: readonly string[];\n\n /**\n * Custom properties its own nodes resolve through, anywhere in the suite.\n *\n * The set a token movement is intersected against, which is how \"`--va-space-3`\n * moved\" becomes \"`--va-space-3` moved and these four components read it\".\n */\n readonly tokens: readonly string[];\n\n /** Sorted by props digest; the class with unknown props sorts last. */\n readonly classes: readonly PropsClass[];\n}\n\n/**\n * The same component, the same props, the same bytes, in more than one subject.\n *\n * The connected dots. `example` names the narrow subject among the sites when\n * there is one, because that is the artefact a reviewer already has an opinion\n * about — \"this is the Chip from the Chip story\" is a sentence, and \"these four\n * paths share a digest\" is not.\n */\nexport interface Echo {\n readonly component: string;\n readonly props?: Digest;\n readonly rendering: Digest;\n /** At least two, spanning at least two subjects. */\n readonly sites: readonly Site[];\n readonly example?: string;\n}\n\n\nexport interface SubjectComposition {\n readonly subject: string;\n readonly instances: readonly ComponentInstance[];\n\n /**\n * The subject as read, kept so a divergence can say *which input* moved.\n *\n * Optional because `composeSubjects` is a fold over instance lists and stays\n * one — a caller holding only a report's sidecars still gets the graph, the\n * echoes and the divergences, and gets them without a `partings` field it\n * would have to explain away as empty.\n */\n readonly snapshot?: SemanticSnapshot;\n}\n\nexport interface Composition {\n /** In the order supplied, which the run guarantees is plan order. */\n readonly subjects: readonly string[];\n /** Sorted by name, code-unit order. */\n readonly components: readonly ComponentEntry[];\n /** Sorted by how many sites each has, widest first, then by component. */\n readonly echoes: readonly Echo[];\n /** Sorted by component name. */\n readonly divergences: readonly Divergence[];\n}\n\n/**\n * Fold a run's per-subject instance lists into the graph.\n *\n * Pure, ordered, and a function of its input alone — the run's report has to be\n * a function of the plan, and a phase that accumulated as a worker pool finished\n * would produce a different artefact from the same suite on a slower machine.\n */\nexport function composeSubjects(subjects: readonly SubjectComposition[]): Composition {\n const entries = new Map<string, Accumulator>();\n const order = subjects.map((subject) => subject.subject);\n const structural = structuralComponents(subjects);\n\n for (const subject of subjects) {\n const example = exampleOf(subject.instances, structural);\n\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n\n const entry = accumulate(entries, instance.component);\n if (entry.subjects.at(-1) !== subject.subject) entry.subjects.push(subject.subject);\n entry.instances += 1;\n if (example === instance.component && entry.examples.at(-1) !== subject.subject) {\n entry.examples.push(subject.subject);\n }\n if (instance.within !== undefined) entry.within.add(instance.within);\n if (instance.createdBy !== undefined) entry.createdBy.add(instance.createdBy);\n for (const child of instance.renders) entry.renders.add(child);\n for (const token of instance.tokens) entry.tokens.add(token);\n\n const key = instance.props ?? UNKNOWN_PROPS;\n const byProps = entry.classes.get(key) ?? new Map<Digest, MutableRendering>();\n const rendering = byProps.get(instance.rendering) ?? {\n rendering: instance.rendering,\n structure: instance.structure,\n semantics: instance.semantics,\n text: instance.text,\n style: instance.style,\n renders: instance.renders,\n sites: [],\n };\n rendering.sites.push({\n subject: subject.subject,\n path: instance.path,\n depth: instance.depth,\n ...(instance.within === undefined ? {} : { within: instance.within }),\n ...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),\n });\n byProps.set(instance.rendering, rendering);\n entry.classes.set(key, byProps);\n }\n }\n\n const components = [...entries.entries()]\n .map(([component, entry]) => ({\n component,\n subjects: entry.subjects,\n instances: entry.instances,\n examples: entry.examples,\n within: [...entry.within].sort(byCodeUnit),\n createdBy: [...entry.createdBy].sort(byCodeUnit),\n renders: [...entry.renders].sort(byCodeUnit),\n tokens: [...entry.tokens].sort(byCodeUnit),\n classes: [...entry.classes.entries()]\n .sort(([a], [b]) => byCodeUnit(a, b))\n .map(([props, renderings]) => ({\n ...(props === UNKNOWN_PROPS ? {} : { props }),\n renderings: [...renderings.values()].sort(widestFirst),\n })),\n }))\n .sort((a, b) => byCodeUnit(a.component, b.component));\n\n const snapshots = new Map<string, SemanticSnapshot>();\n for (const subject of subjects) {\n if (subject.snapshot !== undefined) snapshots.set(subject.subject, subject.snapshot);\n }\n\n return {\n subjects: order,\n components,\n echoes: echoesOf(components),\n divergences: divergencesOf(components, snapshots),\n };\n}\n\n/**\n * Every rendering shared by two or more subjects.\n *\n * Two instances of one component inside a *single* subject sharing a rendering\n * is ordinary and says nothing — a list of three identical chips is three\n * identical chips. The finding is a rendering that survives being mounted\n * somewhere else, because that is the one a second subject is watching.\n */\nfunction echoesOf(components: readonly ComponentEntry[]): readonly Echo[] {\n const echoes: Echo[] = [];\n\n for (const entry of components) {\n for (const group of entry.classes) {\n for (const rendering of group.renderings) {\n const spread = new Set(rendering.sites.map((site) => site.subject));\n if (spread.size < 2) continue;\n\n const example = entry.examples.find((subject) => spread.has(subject));\n echoes.push({\n component: entry.component,\n ...(group.props === undefined ? {} : { props: group.props }),\n rendering: rendering.rendering,\n sites: rendering.sites,\n ...(example === undefined ? {} : { example }),\n });\n }\n }\n }\n\n return echoes.sort(\n (a, b) => b.sites.length - a.sites.length || byCodeUnit(a.component, b.component),\n );\n}\n\n\n/**\n * Components more than half the suite mounts.\n *\n * Structure, not subject matter. A harness wrapper, a theme or store provider,\n * a portal root and an HOC that every screen is wrapped in are all mounted by\n * nearly every subject, and none of them is what any subject is about. The test\n * is a count over the suite rather than a list of names, which is the only form\n * that survives contact with a real application: nothing here knows that\n * `withStyles(x)` is an HOC, that a minified `aL` is a decorator, or that a\n * class component is a context consumer, and nothing here needs to.\n *\n * More than half, because that is where the claim becomes checkable. A\n * component two thirds of a suite mounts cannot be what distinguishes a subject\n * from its neighbours, whatever it is called.\n */\nexport function structuralComponents(subjects: readonly SubjectComposition[]): ReadonlySet<string> {\n const mounts = new Map<string, number>();\n for (const subject of subjects) {\n const mounted = new Set(subject.instances.filter(attributed).map((instance) => instance.component));\n for (const component of mounted) mounts.set(component, (mounts.get(component) ?? 0) + 1);\n }\n\n const structural = new Set<string>();\n for (const [component, held] of mounts) {\n if (held * 2 > subjects.length) structural.add(component);\n }\n return structural;\n}\n\n/**\n * The component a subject exists to show, when one component does.\n *\n * The shallowest attributed boundary that is not structure, and only when it is\n * alone at its depth. A story usually mounts one thing; a page mounts a layout\n * that mounts several, and calling the first of them the subject's component\n * would be picking a winner out of document order.\n *\n * The descent is the whole of the difference between this and the obvious rule.\n * Taking the shallowest boundary outright answers with whatever the harness\n * mounted first, which on a real suite is one value for every subject in it —\n * `Wrapper` across a component library, a minified decorator root across a\n * built Storybook — and a field with one value is a field that says nothing. A\n * depth every one of whose names is structure is passed through however many\n * names it has, because a harness that mounts a provider beside a portal root\n * has still not said what the subject is about.\n *\n * Where the descent finds nothing it answers as the shallowest rule would, so\n * this can name more subjects than that rule and never fewer.\n */\nfunction exampleOf(\n instances: readonly ComponentInstance[],\n structural: ReadonlySet<string>,\n): string | undefined {\n const held = instances.filter(attributed);\n const depths = [...new Set(held.map((instance) => instance.depth))].sort((a, b) => a - b);\n\n for (const depth of depths) {\n const names = new Set(held.filter((instance) => instance.depth === depth).map((i) => i.component));\n const matter = [...names].filter((name) => !structural.has(name));\n if (matter.length === 0) continue;\n return matter.length === 1 ? matter[0] : shallowestOf(held, depths[0]);\n }\n\n return shallowestOf(held, depths[0]);\n}\n\n/** The shallowest boundary when it is alone at its depth: the rule before the descent. */\nfunction shallowestOf(held: readonly ComponentInstance[], shallowest: number | undefined): string | undefined {\n if (shallowest === undefined) return undefined;\n const leads = held.filter((instance) => instance.depth === shallowest);\n const names = new Set(leads.map((instance) => instance.component));\n return names.size === 1 ? leads[0]?.component : undefined;\n}\n\n/**\n * The key an instance with no props digest is filed under.\n *\n * A sentinel rather than `undefined`, because a `Map` keyed on `Digest |\n * undefined` reads as though unknown props were a props class like any other,\n * and every consumer would then have to remember that it is not. It sorts last\n * by construction — no digest starts with a space.\n */\nconst UNKNOWN_PROPS = ' unknown';\n\ninterface Accumulator {\n readonly subjects: string[];\n instances: number;\n readonly examples: string[];\n readonly within: Set<string>;\n readonly createdBy: Set<string>;\n readonly renders: Set<string>;\n readonly tokens: Set<string>;\n readonly classes: Map<string, Map<Digest, MutableRendering>>;\n}\n\n/** A `Rendering` while its sites are still being collected. */\ninterface MutableRendering extends Omit<Rendering, 'sites'> {\n readonly sites: Site[];\n}\n\nfunction accumulate(entries: Map<string, Accumulator>, component: string): Accumulator {\n const existing = entries.get(component);\n if (existing !== undefined) return existing;\n\n const created: Accumulator = {\n subjects: [],\n instances: 0,\n examples: [],\n within: new Set(),\n createdBy: new Set(),\n renders: new Set(),\n tokens: new Set(),\n classes: new Map(),\n };\n entries.set(component, created);\n return created;\n}\n\nfunction widestFirst(a: Rendering, b: Rendering): number {\n return b.sites.length - a.sites.length || byCodeUnit(a.rendering, b.rendering);\n}\n\n/**\n * Code-unit order, never `localeCompare`.\n *\n * The same rule `hashComponents` sorts by, for the same reason: this output\n * reaches a report that is committed, diffed and read back on another runner,\n * and a locale-aware comparison makes the byte order a promise about `LANG`.\n */\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
@@ -19,7 +19,7 @@ export { isolateRegions, subtractRegions, excludedBoxes } from './mask.js';
19
19
  export type { ChangeMask, DiffRegion, ExcludedBox, Isolation, IsolationOptions, Subtraction, } from './mask.js';
20
20
  export { attributeRegions, rankRegions } from './region.js';
21
21
  export type { AttributedRegion, AttributionOptions, RankedRegion } from './region.js';
22
- export { resolveSource, formatSource, indexSource, mergeSourceIndexes, overlaySourceIndex, } from './source.js';
22
+ export { resolveSource, formatSource, mergeSourceIndexes, overlaySourceIndex, } from './source.js';
23
23
  export type { SourceIndex, SourceRef, Resolution } from './source.js';
24
24
  export { parseStackFrames, isVendorPath, writerLocationOf } from './stack.js';
25
25
  export type { StackFrame } from './stack.js';
@@ -36,6 +36,8 @@ export type { ClosureSite, NodeClosure, SharedClosure, SharedClosureOptions, Sub
36
36
  export { composeSubjects } from './composition.js';
37
37
  export type { Composition, ComponentEntry, Echo, PropsClass, Rendering, Site, SubjectComposition, } from './composition.js';
38
38
  export type { Divergence, DivergenceParting } from './divergence.js';
39
+ export { piecesOf } from './subject-pieces.js';
40
+ export type { Residue, SubjectPieces, SubjectShare } from './subject-pieces.js';
39
41
  export { lexiconOf, lexiconOfValues, lexiconValuesOf, structureOf, withExamples, LEXICON_CAP } from './lexicon.js';
40
42
  export { LANDMARK_CAP } from './landmark.js';
41
43
  export type { Landmark } from './landmark.js';
@@ -16,7 +16,7 @@
16
16
  export { locate } from './locate.js';
17
17
  export { isolateRegions, subtractRegions, excludedBoxes } from './mask.js';
18
18
  export { attributeRegions, rankRegions } from './region.js';
19
- export { resolveSource, formatSource, indexSource, mergeSourceIndexes, overlaySourceIndex, } from './source.js';
19
+ export { resolveSource, formatSource, mergeSourceIndexes, overlaySourceIndex, } from './source.js';
20
20
  export { parseStackFrames, isVendorPath, writerLocationOf } from './stack.js';
21
21
  export { parseSourceMap, originalPositionFor, sourceMappingUrlOf, inlineSourceMapOf, } from './source-map.js';
22
22
  export { createCallSiteResolver, locateProvenance, locateSites } from './call-site.js';
@@ -24,6 +24,7 @@ export { hashComponents, causesBetween, movedBands, movedBandsBetween, bandsBetw
24
24
  export { componentInstances, attributed } from './instances.js';
25
25
  export { nodeClosures, sharedClosures } from './closure.js';
26
26
  export { composeSubjects } from './composition.js';
27
+ export { piecesOf } from './subject-pieces.js';
27
28
  export { lexiconOf, lexiconOfValues, lexiconValuesOf, structureOf, withExamples, LEXICON_CAP } from './lexicon.js';
28
29
  export { LANDMARK_CAP } from './landmark.js';
29
30
  export { attributeMovement } from './movement.js';