@variance-authority/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +169 -0
  4. package/dist/artifact.d.ts +47 -0
  5. package/dist/artifact.js +2 -0
  6. package/dist/artifact.js.map +1 -0
  7. package/dist/attribute/because.d.ts +43 -0
  8. package/dist/attribute/because.js +64 -0
  9. package/dist/attribute/because.js.map +1 -0
  10. package/dist/attribute/boundary.d.ts +191 -0
  11. package/dist/attribute/boundary.js +328 -0
  12. package/dist/attribute/boundary.js.map +1 -0
  13. package/dist/attribute/call-site.d.ts +96 -0
  14. package/dist/attribute/call-site.js +183 -0
  15. package/dist/attribute/call-site.js.map +1 -0
  16. package/dist/attribute/component-hash.d.ts +188 -0
  17. package/dist/attribute/component-hash.js +312 -0
  18. package/dist/attribute/component-hash.js.map +1 -0
  19. package/dist/attribute/composition-fixture.d.ts +18 -0
  20. package/dist/attribute/composition-fixture.js +53 -0
  21. package/dist/attribute/composition-fixture.js.map +1 -0
  22. package/dist/attribute/composition.d.ts +182 -0
  23. package/dist/attribute/composition.js +169 -0
  24. package/dist/attribute/composition.js.map +1 -0
  25. package/dist/attribute/control.d.ts +43 -0
  26. package/dist/attribute/control.js +54 -0
  27. package/dist/attribute/control.js.map +1 -0
  28. package/dist/attribute/coverage.d.ts +99 -0
  29. package/dist/attribute/coverage.js +86 -0
  30. package/dist/attribute/coverage.js.map +1 -0
  31. package/dist/attribute/divergence.d.ts +102 -0
  32. package/dist/attribute/divergence.js +152 -0
  33. package/dist/attribute/divergence.js.map +1 -0
  34. package/dist/attribute/index.d.ts +43 -0
  35. package/dist/attribute/index.js +29 -0
  36. package/dist/attribute/index.js.map +1 -0
  37. package/dist/attribute/instability.d.ts +89 -0
  38. package/dist/attribute/instability.js +183 -0
  39. package/dist/attribute/instability.js.map +1 -0
  40. package/dist/attribute/instances.d.ts +145 -0
  41. package/dist/attribute/instances.js +140 -0
  42. package/dist/attribute/instances.js.map +1 -0
  43. package/dist/attribute/locate.d.ts +22 -0
  44. package/dist/attribute/locate.js +100 -0
  45. package/dist/attribute/locate.js.map +1 -0
  46. package/dist/attribute/mask.d.ts +156 -0
  47. package/dist/attribute/mask.js +201 -0
  48. package/dist/attribute/mask.js.map +1 -0
  49. package/dist/attribute/movement.d.ts +211 -0
  50. package/dist/attribute/movement.js +181 -0
  51. package/dist/attribute/movement.js.map +1 -0
  52. package/dist/attribute/region.d.ts +188 -0
  53. package/dist/attribute/region.js +144 -0
  54. package/dist/attribute/region.js.map +1 -0
  55. package/dist/attribute/source-map.d.ts +98 -0
  56. package/dist/attribute/source-map.js +264 -0
  57. package/dist/attribute/source-map.js.map +1 -0
  58. package/dist/attribute/source.d.ts +77 -0
  59. package/dist/attribute/source.js +95 -0
  60. package/dist/attribute/source.js.map +1 -0
  61. package/dist/attribute/stack.d.ts +80 -0
  62. package/dist/attribute/stack.js +108 -0
  63. package/dist/attribute/stack.js.map +1 -0
  64. package/dist/compare/band.d.ts +85 -0
  65. package/dist/compare/band.js +70 -0
  66. package/dist/compare/band.js.map +1 -0
  67. package/dist/compare/cascade.d.ts +45 -0
  68. package/dist/compare/cascade.js +71 -0
  69. package/dist/compare/cascade.js.map +1 -0
  70. package/dist/compare/derive.d.ts +78 -0
  71. package/dist/compare/derive.js +39 -0
  72. package/dist/compare/derive.js.map +1 -0
  73. package/dist/compare/diff/attribution.d.ts +23 -0
  74. package/dist/compare/diff/attribution.js +244 -0
  75. package/dist/compare/diff/attribution.js.map +1 -0
  76. package/dist/compare/diff/compare-nodes.d.ts +15 -0
  77. package/dist/compare/diff/compare-nodes.js +227 -0
  78. package/dist/compare/diff/compare-nodes.js.map +1 -0
  79. package/dist/compare/diff/components.d.ts +20 -0
  80. package/dist/compare/diff/components.js +76 -0
  81. package/dist/compare/diff/components.js.map +1 -0
  82. package/dist/compare/diff/delta.d.ts +165 -0
  83. package/dist/compare/diff/delta.js +12 -0
  84. package/dist/compare/diff/delta.js.map +1 -0
  85. package/dist/compare/diff/index.d.ts +69 -0
  86. package/dist/compare/diff/index.js +101 -0
  87. package/dist/compare/diff/index.js.map +1 -0
  88. package/dist/compare/diff/match.d.ts +37 -0
  89. package/dist/compare/diff/match.js +167 -0
  90. package/dist/compare/diff/match.js.map +1 -0
  91. package/dist/compare/explain.d.ts +23 -0
  92. package/dist/compare/explain.js +190 -0
  93. package/dist/compare/explain.js.map +1 -0
  94. package/dist/compare/holding-diff.d.ts +25 -0
  95. package/dist/compare/holding-diff.js +74 -0
  96. package/dist/compare/holding-diff.js.map +1 -0
  97. package/dist/compare/impact.d.ts +56 -0
  98. package/dist/compare/impact.js +124 -0
  99. package/dist/compare/impact.js.map +1 -0
  100. package/dist/compare/index.d.ts +30 -0
  101. package/dist/compare/index.js +22 -0
  102. package/dist/compare/index.js.map +1 -0
  103. package/dist/compare/instance.d.ts +30 -0
  104. package/dist/compare/instance.js +145 -0
  105. package/dist/compare/instance.js.map +1 -0
  106. package/dist/compare/observability.d.ts +49 -0
  107. package/dist/compare/observability.js +57 -0
  108. package/dist/compare/observability.js.map +1 -0
  109. package/dist/compare/parting.d.ts +179 -0
  110. package/dist/compare/parting.js +244 -0
  111. package/dist/compare/parting.js.map +1 -0
  112. package/dist/compare/slice.d.ts +74 -0
  113. package/dist/compare/slice.js +79 -0
  114. package/dist/compare/slice.js.map +1 -0
  115. package/dist/compare/value.d.ts +60 -0
  116. package/dist/compare/value.js +111 -0
  117. package/dist/compare/value.js.map +1 -0
  118. package/dist/format/accessibility.d.ts +27 -0
  119. package/dist/format/accessibility.js +17 -0
  120. package/dist/format/accessibility.js.map +1 -0
  121. package/dist/format/canonical.d.ts +38 -0
  122. package/dist/format/canonical.js +65 -0
  123. package/dist/format/canonical.js.map +1 -0
  124. package/dist/format/capture.d.ts +212 -0
  125. package/dist/format/capture.js +2 -0
  126. package/dist/format/capture.js.map +1 -0
  127. package/dist/format/document.d.ts +279 -0
  128. package/dist/format/document.js +65 -0
  129. package/dist/format/document.js.map +1 -0
  130. package/dist/format/environment.d.ts +120 -0
  131. package/dist/format/environment.js +53 -0
  132. package/dist/format/environment.js.map +1 -0
  133. package/dist/format/hash.d.ts +45 -0
  134. package/dist/format/hash.js +41 -0
  135. package/dist/format/hash.js.map +1 -0
  136. package/dist/format/holding.d.ts +134 -0
  137. package/dist/format/holding.js +2 -0
  138. package/dist/format/holding.js.map +1 -0
  139. package/dist/format/index.d.ts +39 -0
  140. package/dist/format/index.js +25 -0
  141. package/dist/format/index.js.map +1 -0
  142. package/dist/format/intervention.d.ts +87 -0
  143. package/dist/format/intervention.js +11 -0
  144. package/dist/format/intervention.js.map +1 -0
  145. package/dist/format/profile.d.ts +59 -0
  146. package/dist/format/profile.js +66 -0
  147. package/dist/format/profile.js.map +1 -0
  148. package/dist/format/provenance.d.ts +179 -0
  149. package/dist/format/provenance.js +222 -0
  150. package/dist/format/provenance.js.map +1 -0
  151. package/dist/format/sha256.d.ts +34 -0
  152. package/dist/format/sha256.js +152 -0
  153. package/dist/format/sha256.js.map +1 -0
  154. package/dist/format/snapshot.d.ts +263 -0
  155. package/dist/format/snapshot.js +2 -0
  156. package/dist/format/snapshot.js.map +1 -0
  157. package/dist/format/stabilize.d.ts +190 -0
  158. package/dist/format/stabilize.js +333 -0
  159. package/dist/format/stabilize.js.map +1 -0
  160. package/dist/format/tier.d.ts +19 -0
  161. package/dist/format/tier.js +11 -0
  162. package/dist/format/tier.js.map +1 -0
  163. package/dist/format/value.d.ts +115 -0
  164. package/dist/format/value.js +182 -0
  165. package/dist/format/value.js.map +1 -0
  166. package/dist/format/wiring.d.ts +115 -0
  167. package/dist/format/wiring.js +60 -0
  168. package/dist/format/wiring.js.map +1 -0
  169. package/dist/index.d.ts +35 -0
  170. package/dist/index.js +35 -0
  171. package/dist/index.js.map +1 -0
  172. package/dist/judge/docket.d.ts +85 -0
  173. package/dist/judge/docket.js +162 -0
  174. package/dist/judge/docket.js.map +1 -0
  175. package/dist/judge/fingerprint.d.ts +53 -0
  176. package/dist/judge/fingerprint.js +144 -0
  177. package/dist/judge/fingerprint.js.map +1 -0
  178. package/dist/judge/ignore.d.ts +206 -0
  179. package/dist/judge/ignore.js +237 -0
  180. package/dist/judge/ignore.js.map +1 -0
  181. package/dist/judge/index.d.ts +44 -0
  182. package/dist/judge/index.js +34 -0
  183. package/dist/judge/index.js.map +1 -0
  184. package/dist/judge/inspect.d.ts +148 -0
  185. package/dist/judge/inspect.js +208 -0
  186. package/dist/judge/inspect.js.map +1 -0
  187. package/dist/judge/intent.d.ts +146 -0
  188. package/dist/judge/intent.js +150 -0
  189. package/dist/judge/intent.js.map +1 -0
  190. package/dist/judge/locale.d.ts +111 -0
  191. package/dist/judge/locale.js +247 -0
  192. package/dist/judge/locale.js.map +1 -0
  193. package/dist/judge/scope.d.ts +41 -0
  194. package/dist/judge/scope.js +47 -0
  195. package/dist/judge/scope.js.map +1 -0
  196. package/dist/judge/sensitivity.d.ts +202 -0
  197. package/dist/judge/sensitivity.js +152 -0
  198. package/dist/judge/sensitivity.js.map +1 -0
  199. package/dist/judge/trail.d.ts +122 -0
  200. package/dist/judge/trail.js +96 -0
  201. package/dist/judge/trail.js.map +1 -0
  202. package/dist/judge/verdict.d.ts +39 -0
  203. package/dist/judge/verdict.js +32 -0
  204. package/dist/judge/verdict.js.map +1 -0
  205. package/dist/plan/index.d.ts +12 -0
  206. package/dist/plan/index.js +11 -0
  207. package/dist/plan/index.js.map +1 -0
  208. package/dist/plan/tool.d.ts +85 -0
  209. package/dist/plan/tool.js +87 -0
  210. package/dist/plan/tool.js.map +1 -0
  211. package/dist/relate/graph.d.ts +190 -0
  212. package/dist/relate/graph.js +234 -0
  213. package/dist/relate/graph.js.map +1 -0
  214. package/dist/relate/index.d.ts +16 -0
  215. package/dist/relate/index.js +16 -0
  216. package/dist/relate/index.js.map +1 -0
  217. package/dist/relate/merkle.d.ts +110 -0
  218. package/dist/relate/merkle.js +246 -0
  219. package/dist/relate/merkle.js.map +1 -0
  220. package/dist/relate/reach.d.ts +65 -0
  221. package/dist/relate/reach.js +111 -0
  222. package/dist/relate/reach.js.map +1 -0
  223. package/dist/relate/records.d.ts +129 -0
  224. package/dist/relate/records.js +112 -0
  225. package/dist/relate/records.js.map +1 -0
  226. package/dist/rules/index.d.ts +19 -0
  227. package/dist/rules/index.js +17 -0
  228. package/dist/rules/index.js.map +1 -0
  229. package/dist/rules/normalize/alias.d.ts +50 -0
  230. package/dist/rules/normalize/alias.js +111 -0
  231. package/dist/rules/normalize/alias.js.map +1 -0
  232. package/dist/rules/normalize/cascade.d.ts +85 -0
  233. package/dist/rules/normalize/cascade.js +324 -0
  234. package/dist/rules/normalize/cascade.js.map +1 -0
  235. package/dist/rules/normalize/color.d.ts +30 -0
  236. package/dist/rules/normalize/color.js +185 -0
  237. package/dist/rules/normalize/color.js.map +1 -0
  238. package/dist/rules/normalize/fixture.d.ts +83 -0
  239. package/dist/rules/normalize/fixture.js +74 -0
  240. package/dist/rules/normalize/fixture.js.map +1 -0
  241. package/dist/rules/normalize/index.d.ts +39 -0
  242. package/dist/rules/normalize/index.js +268 -0
  243. package/dist/rules/normalize/index.js.map +1 -0
  244. package/dist/rules/normalize/project.d.ts +42 -0
  245. package/dist/rules/normalize/project.js +83 -0
  246. package/dist/rules/normalize/project.js.map +1 -0
  247. package/dist/rules/normalize/shorthand.d.ts +38 -0
  248. package/dist/rules/normalize/shorthand.js +388 -0
  249. package/dist/rules/normalize/shorthand.js.map +1 -0
  250. package/dist/rules/normalize/value.d.ts +23 -0
  251. package/dist/rules/normalize/value.js +162 -0
  252. package/dist/rules/normalize/value.js.map +1 -0
  253. package/dist/rules/normalize/wrapper.d.ts +16 -0
  254. package/dist/rules/normalize/wrapper.js +137 -0
  255. package/dist/rules/normalize/wrapper.js.map +1 -0
  256. package/dist/rules/ruleset.d.ts +93 -0
  257. package/dist/rules/ruleset.js +175 -0
  258. package/dist/rules/ruleset.js.map +1 -0
  259. package/mark.svg +30 -0
  260. package/package.json +69 -0
@@ -0,0 +1,145 @@
1
+ import type { Digest } from '../format/hash.js';
2
+ import type { NodePath, SemanticSnapshot } from '../format/snapshot.js';
3
+ /**
4
+ * One digest per component *boundary*, comparable across subjects.
5
+ *
6
+ * `hashComponents` folds every boundary with the same name into one row, which
7
+ * is the right unit for a history — it is the unit an edit moves — and the wrong
8
+ * unit for the question a suite of examples actually poses:
9
+ *
10
+ * > A visual-regression example is a component built from components. The
11
+ * > example *is* a component, at a boundary; the same component appears again,
12
+ * > with the same or different props, inside larger examples. Connecting those
13
+ * > is connecting the dots.
14
+ *
15
+ * Under the aggregate that connection is unmakeable. `Button` in a one-button
16
+ * story digests a list of one shape; `Button` in a page holding three digests a
17
+ * list of three; the two can never be equal, and the fact that one of the three
18
+ * is byte-identical to the story is not recoverable from either digest.
19
+ *
20
+ * So this is the same walk and the same shapes, digested per boundary instead of
21
+ * per name. It is additive: `hashComponents` is untouched and still produces the
22
+ * bytes a sidecar already holds.
23
+ *
24
+ * ## Two things had to change for a digest to cross a subject boundary
25
+ *
26
+ * **Aliases are subject-ordinal.** The normalizer replaces every id with `#a0`,
27
+ * `#a1`, … in document order *across the whole subject* (ADR-0003). That is
28
+ * exactly right for a subject and fatal across two: the same field rendered in a
29
+ * story and on a page gets `#a0` in one and `#a7` in the other, so its structure
30
+ * digests differ and the dots stay unconnected — for a reason that is an
31
+ * artefact of where else the component happened to be mounted. This re-aliases
32
+ * to `#b0`, `#b1`, … in the boundary's own document order, which is the same
33
+ * argument `shapeOf` already makes about paths: an address that shifts when
34
+ * something unrelated moves must not reach a hash.
35
+ *
36
+ * The association survives, which is the property that made aliasing worth doing
37
+ * in the first place. A `for` that points at an input inside the boundary keeps
38
+ * pointing at it. A reference that escapes the boundary keeps its own slot and
39
+ * points at nothing in particular — from inside, one foreign target is
40
+ * indistinguishable from another, and pretending otherwise would report a
41
+ * difference the component cannot see.
42
+ *
43
+ * **Geometry is left out of the joining digest.** A rect is absolute page
44
+ * coordinates, so two identical renderings in two subjects disagree on it
45
+ * always. `rendering` is the four content digests; the geometry digest is
46
+ * carried beside it, where a caller comparing two instances *within* one subject
47
+ * can still read it.
48
+ */
49
+ /** One component boundary, hashed. */
50
+ export interface ComponentInstance {
51
+ readonly component: string;
52
+ /**
53
+ * Where it sits in this subject. Never hashed — see `shapeOf`.
54
+ *
55
+ * Carried so a finding can be pointed at, which is the whole reason a
56
+ * per-instance record is worth having: "the third `Chip`" is a sentence a
57
+ * reviewer can act on and `instances: 3` is not.
58
+ */
59
+ readonly path: NodePath;
60
+ /** Boundaries between this one and the subject root. The root is `0`. */
61
+ readonly depth: number;
62
+ /** The enclosing boundary's component. Absent on the subject root. */
63
+ readonly within?: string;
64
+ /**
65
+ * Who placed this boundary's element, when it differs from `within`.
66
+ *
67
+ * `within` says which component this one *sits inside* and `createdBy` says
68
+ * which component *wrote the element*, and the two diverge wherever a
69
+ * component is passed as a prop and rendered somewhere else (ADR-0007). That
70
+ * divergence is a fact about composition, so it is kept rather than resolved.
71
+ */
72
+ readonly createdBy?: string;
73
+ /**
74
+ * Digest of the props this boundary received, from `OwnerFrame.propsDigest`.
75
+ *
76
+ * Absent when the collector supplied no provenance for the boundary root —
77
+ * absent, never a digest of nothing, because "rendered with unknown inputs"
78
+ * and "rendered with no inputs" are different claims and only one of them
79
+ * licenses a join.
80
+ *
81
+ * It is one-way. Two instances can be shown to have received different props;
82
+ * *which* prop differed is not recoverable, and nothing here pretends it is.
83
+ */
84
+ readonly props?: Digest;
85
+ /**
86
+ * The four content digests, together. The join key.
87
+ *
88
+ * Two instances with the same `rendering` rendered the same thing, wherever
89
+ * they were and whatever else was on the page with them.
90
+ */
91
+ readonly rendering: Digest;
92
+ readonly structure: Digest;
93
+ readonly semantics: Digest;
94
+ readonly text: Digest;
95
+ readonly style: Digest;
96
+ /** Absent under a profile without layout — absent, never empty (ADR-0002). */
97
+ readonly geometry?: Digest;
98
+ /**
99
+ * How the framework holds this boundary: hook shape, wrappers, context
100
+ * subscriptions, reconciliation keys. See `Wiring`.
101
+ *
102
+ * Deliberately **not** folded into `rendering`, for the same reason `geometry`
103
+ * is not: `rendering` is the four content digests and its contract is that two
104
+ * instances sharing it rendered the same thing. A component that gained a
105
+ * `memo()` renders the same thing. Folding wiring in would make a performance
106
+ * annotation read as a visual regression and would re-baseline every subject
107
+ * on the first run after a framework adapter was wired up — which is the
108
+ * surest way to make a new dimension the thing people turn off.
109
+ *
110
+ * Absent when no adapter supplied wiring for any node in this boundary.
111
+ * Absent, never a digest of nulls, because "no framework here" and "a framework
112
+ * nobody could read" must not compare equal (ADR-0002).
113
+ */
114
+ readonly wiring?: Digest;
115
+ /** Child boundaries, in document order. The edges of the component graph. */
116
+ readonly renders: readonly string[];
117
+ /** Nodes this boundary owns, counting its own root. */
118
+ readonly nodes: number;
119
+ /**
120
+ * Custom properties this boundary's own nodes resolved through, sorted.
121
+ *
122
+ * The join between the token axis and the component axis. A run that reports
123
+ * `--va-space-3` moved can now say which components read it, rather than
124
+ * leaving a reader to guess from a colour.
125
+ */
126
+ readonly tokens: readonly string[];
127
+ }
128
+ /**
129
+ * Every component boundary in a subject, in document order.
130
+ *
131
+ * Document order, not name order, because unlike `hashComponents` this list is
132
+ * not a set of rows keyed by name — it is a walk, and its order is the only
133
+ * thing that makes "the third `Chip`" mean anything.
134
+ */
135
+ export declare function componentInstances(snapshot: SemanticSnapshot): readonly ComponentInstance[];
136
+ /**
137
+ * Whether an instance is worth joining on at all.
138
+ *
139
+ * `(unattributed)` is one bucket per subject collecting every node whose
140
+ * provenance chain broke, anywhere. Two subjects' buckets hold unrelated parts
141
+ * of two pages, so an equality between them means nothing and an inequality
142
+ * means less — it is the only name in the vocabulary that is not a component.
143
+ */
144
+ export declare function attributed(instance: ComponentInstance): boolean;
145
+ //# sourceMappingURL=instances.d.ts.map
@@ -0,0 +1,140 @@
1
+ import { digestValue } from '../format/hash.js';
2
+ import { ID_REFERENCE_ATTRIBUTES, ID_REFERENCE_LIST_ATTRIBUTES } from '../rules/ruleset.js';
3
+ import { boundaries, holds, shapeOf, UNATTRIBUTED, } from './boundary.js';
4
+ /**
5
+ * Every component boundary in a subject, in document order.
6
+ *
7
+ * Document order, not name order, because unlike `hashComponents` this list is
8
+ * not a set of rows keyed by name — it is a walk, and its order is the only
9
+ * thing that makes "the third `Chip`" mean anything.
10
+ */
11
+ export function componentInstances(snapshot) {
12
+ const layout = snapshot.profile.layout;
13
+ return boundaries(snapshot.root).map((boundary) => {
14
+ const shape = shapeOf(boundary, layout, localAliases(boundary));
15
+ const structure = digestValue(shape.structure);
16
+ const semantics = digestValue(shape.semantics);
17
+ const text = digestValue(shape.text);
18
+ const style = digestValue(shape.style);
19
+ return {
20
+ component: boundary.component,
21
+ path: boundary.node.path,
22
+ depth: boundary.depth,
23
+ ...(boundary.within === undefined ? {} : { within: boundary.within }),
24
+ ...(boundary.placedBy === undefined ? {} : { createdBy: boundary.placedBy }),
25
+ ...(boundary.props === undefined ? {} : { props: boundary.props }),
26
+ rendering: digestValue([structure, semantics, text, style]),
27
+ structure,
28
+ semantics,
29
+ text,
30
+ style,
31
+ ...(layout ? { geometry: digestValue(shape.geometry) } : {}),
32
+ // Only when something reported wiring. A boundary of pure nulls is a
33
+ // boundary nobody read, and hashing it would give a plain-DOM page a
34
+ // `wiring` digest to be compared against a React page's.
35
+ ...(observed(shape.wiring) ? { wiring: digestValue(shape.wiring) } : {}),
36
+ renders: shape.renders,
37
+ nodes: shape.nodes,
38
+ tokens: shape.tokens,
39
+ };
40
+ });
41
+ }
42
+ /**
43
+ * Whether a per-node band list holds anything at all.
44
+ *
45
+ * The band pushes one entry per node so that position stays meaningful, which
46
+ * means a boundary under no framework adapter produces a full-length list of
47
+ * `null`. That is a well-formed value and it hashes to something, and something
48
+ * is exactly what it must not produce — the digest would then say "this boundary
49
+ * has known wiring, and it is nothing", which joins against every other
50
+ * unread boundary in the corpus.
51
+ */
52
+ function observed(band) {
53
+ return Array.isArray(band) && band.some((entry) => entry !== null);
54
+ }
55
+ /**
56
+ * Whether an instance is worth joining on at all.
57
+ *
58
+ * `(unattributed)` is one bucket per subject collecting every node whose
59
+ * provenance chain broke, anywhere. Two subjects' buckets hold unrelated parts
60
+ * of two pages, so an equality between them means nothing and an inequality
61
+ * means less — it is the only name in the vocabulary that is not a component.
62
+ */
63
+ export function attributed(instance) {
64
+ return instance.component !== UNATTRIBUTED;
65
+ }
66
+ const REFERENCE = new Set(ID_REFERENCE_ATTRIBUTES);
67
+ const REFERENCE_LIST = new Set(ID_REFERENCE_LIST_ATTRIBUTES);
68
+ const ALIAS = /^#(?:a\d+|extern:.*)$/;
69
+ /**
70
+ * A boundary-local alias space, assigned in the boundary's own document order.
71
+ *
72
+ * Built in a pass of its own, ahead of hashing, for the reason `buildAliasMap`
73
+ * takes two: a reference may precede its definition, and an alias whose slot
74
+ * depended on which side was seen first would differ between two identical
75
+ * renderings that happened to be reached in a different order. Here the walk
76
+ * order is fixed, so one pass over the boundary's own nodes in document order —
77
+ * definition or reference, whichever is met first — is deterministic and enough.
78
+ *
79
+ * Stops at nested boundaries, exactly as `shapeOf` does. A child component's
80
+ * internal ids are its own; folding them into the parent's alias space would
81
+ * make the parent's structure digest move when the child renumbered internally,
82
+ * which is the ancestor-moves-on-every-leaf-edit failure the boundary rule
83
+ * exists to prevent.
84
+ */
85
+ function localAliases(boundary) {
86
+ const local = new Map();
87
+ const take = (value) => {
88
+ if (!ALIAS.test(value) || local.has(value))
89
+ return;
90
+ local.set(value, `#b${local.size}`);
91
+ };
92
+ const walk = (node) => {
93
+ if (node.alias !== undefined)
94
+ take(node.alias);
95
+ for (const [name, value] of Object.entries(node.attributes)) {
96
+ if (REFERENCE_LIST.has(name))
97
+ for (const part of value.split(/\s+/))
98
+ take(part);
99
+ else if (REFERENCE.has(name) || name === 'href' || name === 'xlink:href')
100
+ take(value);
101
+ }
102
+ for (const value of Object.values(node.style))
103
+ for (const found of urlAliases(value))
104
+ take(found);
105
+ for (const value of Object.values(node.tokens ?? {}))
106
+ for (const found of urlAliases(value))
107
+ take(found);
108
+ for (const child of node.children)
109
+ if (holds(boundary, child))
110
+ walk(child);
111
+ };
112
+ walk(boundary.node);
113
+ // Unmapped values pass through unchanged. A reference this walk never reached
114
+ // — one inside a nested boundary, reaching back out — is left as the subject
115
+ // alias it already was, which is visible and wrong in the same direction as
116
+ // doing nothing, rather than silently colliding with a local slot.
117
+ const rewrite = (value) => local.get(value) ?? value;
118
+ return {
119
+ alias: rewrite,
120
+ style: (value) => value.replace(/url\(\s*(['"]?)#([^'")]+)\1\s*\)/g, (match, quote, id) => {
121
+ const mapped = local.get(`#${id}`);
122
+ return mapped === undefined ? match : `url(${quote}${mapped}${quote})`;
123
+ }),
124
+ attribute: (name, value) => {
125
+ if (REFERENCE_LIST.has(name)) {
126
+ return value
127
+ .split(/\s+/)
128
+ .map((part) => rewrite(part))
129
+ .join(' ');
130
+ }
131
+ if (REFERENCE.has(name) || name === 'href' || name === 'xlink:href')
132
+ return rewrite(value);
133
+ return value;
134
+ },
135
+ };
136
+ }
137
+ function urlAliases(value) {
138
+ return [...value.matchAll(/url\(\s*['"]?#([^'")]+)['"]?\s*\)/g)].map((match) => `#${match[1]}`);
139
+ }
140
+ //# sourceMappingURL=instances.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instances.js","sourceRoot":"","sources":["../../src/attribute/instances.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CanonicalValue } from '../format/canonical.js';\nimport { digestValue } from '../format/hash.js';\nimport type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticNode, SemanticSnapshot } from '../format/snapshot.js';\nimport { ID_REFERENCE_ATTRIBUTES, ID_REFERENCE_LIST_ATTRIBUTES } from '../rules/ruleset.js';\nimport {\n boundaries,\n holds,\n shapeOf,\n UNATTRIBUTED,\n type Boundary,\n type Rename,\n} from './boundary.js';\n\n/**\n * One digest per component *boundary*, comparable across subjects.\n *\n * `hashComponents` folds every boundary with the same name into one row, which\n * is the right unit for a history — it is the unit an edit moves — and the wrong\n * unit for the question a suite of examples actually poses:\n *\n * > A visual-regression example is a component built from components. The\n * > example *is* a component, at a boundary; the same component appears again,\n * > with the same or different props, inside larger examples. Connecting those\n * > is connecting the dots.\n *\n * Under the aggregate that connection is unmakeable. `Button` in a one-button\n * story digests a list of one shape; `Button` in a page holding three digests a\n * list of three; the two can never be equal, and the fact that one of the three\n * is byte-identical to the story is not recoverable from either digest.\n *\n * So this is the same walk and the same shapes, digested per boundary instead of\n * per name. It is additive: `hashComponents` is untouched and still produces the\n * bytes a sidecar already holds.\n *\n * ## Two things had to change for a digest to cross a subject boundary\n *\n * **Aliases are subject-ordinal.** The normalizer replaces every id with `#a0`,\n * `#a1`, … in document order *across the whole subject* (ADR-0003). That is\n * exactly right for a subject and fatal across two: the same field rendered in a\n * story and on a page gets `#a0` in one and `#a7` in the other, so its structure\n * digests differ and the dots stay unconnected — for a reason that is an\n * artefact of where else the component happened to be mounted. This re-aliases\n * to `#b0`, `#b1`, … in the boundary's own document order, which is the same\n * argument `shapeOf` already makes about paths: an address that shifts when\n * something unrelated moves must not reach a hash.\n *\n * The association survives, which is the property that made aliasing worth doing\n * in the first place. A `for` that points at an input inside the boundary keeps\n * pointing at it. A reference that escapes the boundary keeps its own slot and\n * points at nothing in particular — from inside, one foreign target is\n * indistinguishable from another, and pretending otherwise would report a\n * difference the component cannot see.\n *\n * **Geometry is left out of the joining digest.** A rect is absolute page\n * coordinates, so two identical renderings in two subjects disagree on it\n * always. `rendering` is the four content digests; the geometry digest is\n * carried beside it, where a caller comparing two instances *within* one subject\n * can still read it.\n */\n\n/** One component boundary, hashed. */\nexport interface ComponentInstance {\n readonly component: string;\n\n /**\n * Where it sits in this subject. Never hashed — see `shapeOf`.\n *\n * Carried so a finding can be pointed at, which is the whole reason a\n * per-instance record is worth having: \"the third `Chip`\" is a sentence a\n * reviewer can act on and `instances: 3` is not.\n */\n readonly path: NodePath;\n\n /** Boundaries between this one and the subject root. The root is `0`. */\n readonly depth: number;\n\n /** The enclosing boundary's component. Absent on the subject root. */\n readonly within?: string;\n\n /**\n * Who placed this boundary's element, when it differs from `within`.\n *\n * `within` says which component this one *sits inside* and `createdBy` says\n * which component *wrote the element*, and the two diverge wherever a\n * component is passed as a prop and rendered somewhere else (ADR-0007). That\n * divergence is a fact about composition, so it is kept rather than resolved.\n */\n readonly createdBy?: string;\n\n /**\n * Digest of the props this boundary received, from `OwnerFrame.propsDigest`.\n *\n * Absent when the collector supplied no provenance for the boundary root —\n * absent, never a digest of nothing, because \"rendered with unknown inputs\"\n * and \"rendered with no inputs\" are different claims and only one of them\n * licenses a join.\n *\n * It is one-way. Two instances can be shown to have received different props;\n * *which* prop differed is not recoverable, and nothing here pretends it is.\n */\n readonly props?: Digest;\n\n /**\n * The four content digests, together. The join key.\n *\n * Two instances with the same `rendering` rendered the same thing, wherever\n * they were and whatever else was on the page with them.\n */\n readonly rendering: Digest;\n\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n /** Absent under a profile without layout — absent, never empty (ADR-0002). */\n readonly geometry?: Digest;\n\n /**\n * How the framework holds this boundary: hook shape, wrappers, context\n * subscriptions, reconciliation keys. See `Wiring`.\n *\n * Deliberately **not** folded into `rendering`, for the same reason `geometry`\n * is not: `rendering` is the four content digests and its contract is that two\n * instances sharing it rendered the same thing. A component that gained a\n * `memo()` renders the same thing. Folding wiring in would make a performance\n * annotation read as a visual regression and would re-baseline every subject\n * on the first run after a framework adapter was wired up — which is the\n * surest way to make a new dimension the thing people turn off.\n *\n * Absent when no adapter supplied wiring for any node in this boundary.\n * Absent, never a digest of nulls, because \"no framework here\" and \"a framework\n * nobody could read\" must not compare equal (ADR-0002).\n */\n readonly wiring?: Digest;\n\n /** Child boundaries, in document order. The edges of the component graph. */\n readonly renders: readonly string[];\n\n /** Nodes this boundary owns, counting its own root. */\n readonly nodes: number;\n\n /**\n * Custom properties this boundary's own nodes resolved through, sorted.\n *\n * The join between the token axis and the component axis. A run that reports\n * `--va-space-3` moved can now say which components read it, rather than\n * leaving a reader to guess from a colour.\n */\n readonly tokens: readonly string[];\n}\n\n/**\n * Every component boundary in a subject, in document order.\n *\n * Document order, not name order, because unlike `hashComponents` this list is\n * not a set of rows keyed by name — it is a walk, and its order is the only\n * thing that makes \"the third `Chip`\" mean anything.\n */\nexport function componentInstances(snapshot: SemanticSnapshot): readonly ComponentInstance[] {\n const layout = snapshot.profile.layout;\n\n return boundaries(snapshot.root).map((boundary) => {\n const shape = shapeOf(boundary, layout, localAliases(boundary));\n\n const structure = digestValue(shape.structure);\n const semantics = digestValue(shape.semantics);\n const text = digestValue(shape.text);\n const style = digestValue(shape.style);\n\n return {\n component: boundary.component,\n path: boundary.node.path,\n depth: boundary.depth,\n ...(boundary.within === undefined ? {} : { within: boundary.within }),\n ...(boundary.placedBy === undefined ? {} : { createdBy: boundary.placedBy }),\n ...(boundary.props === undefined ? {} : { props: boundary.props }),\n rendering: digestValue([structure, semantics, text, style]),\n structure,\n semantics,\n text,\n style,\n ...(layout ? { geometry: digestValue(shape.geometry) } : {}),\n // Only when something reported wiring. A boundary of pure nulls is a\n // boundary nobody read, and hashing it would give a plain-DOM page a\n // `wiring` digest to be compared against a React page's.\n ...(observed(shape.wiring) ? { wiring: digestValue(shape.wiring) } : {}),\n renders: shape.renders,\n nodes: shape.nodes,\n tokens: shape.tokens,\n };\n });\n}\n\n/**\n * Whether a per-node band list holds anything at all.\n *\n * The band pushes one entry per node so that position stays meaningful, which\n * means a boundary under no framework adapter produces a full-length list of\n * `null`. That is a well-formed value and it hashes to something, and something\n * is exactly what it must not produce — the digest would then say \"this boundary\n * has known wiring, and it is nothing\", which joins against every other\n * unread boundary in the corpus.\n */\nfunction observed(band: CanonicalValue): boolean {\n return Array.isArray(band) && band.some((entry) => entry !== null);\n}\n\n/**\n * Whether an instance is worth joining on at all.\n *\n * `(unattributed)` is one bucket per subject collecting every node whose\n * provenance chain broke, anywhere. Two subjects' buckets hold unrelated parts\n * of two pages, so an equality between them means nothing and an inequality\n * means less — it is the only name in the vocabulary that is not a component.\n */\nexport function attributed(instance: ComponentInstance): boolean {\n return instance.component !== UNATTRIBUTED;\n}\n\nconst REFERENCE = new Set(ID_REFERENCE_ATTRIBUTES);\nconst REFERENCE_LIST = new Set(ID_REFERENCE_LIST_ATTRIBUTES);\nconst ALIAS = /^#(?:a\\d+|extern:.*)$/;\n\n/**\n * A boundary-local alias space, assigned in the boundary's own document order.\n *\n * Built in a pass of its own, ahead of hashing, for the reason `buildAliasMap`\n * takes two: a reference may precede its definition, and an alias whose slot\n * depended on which side was seen first would differ between two identical\n * renderings that happened to be reached in a different order. Here the walk\n * order is fixed, so one pass over the boundary's own nodes in document order —\n * definition or reference, whichever is met first — is deterministic and enough.\n *\n * Stops at nested boundaries, exactly as `shapeOf` does. A child component's\n * internal ids are its own; folding them into the parent's alias space would\n * make the parent's structure digest move when the child renumbered internally,\n * which is the ancestor-moves-on-every-leaf-edit failure the boundary rule\n * exists to prevent.\n */\nfunction localAliases(boundary: Boundary): Rename {\n const local = new Map<string, string>();\n\n const take = (value: string): void => {\n if (!ALIAS.test(value) || local.has(value)) return;\n local.set(value, `#b${local.size}`);\n };\n\n const walk = (node: SemanticNode): void => {\n if (node.alias !== undefined) take(node.alias);\n for (const [name, value] of Object.entries(node.attributes)) {\n if (REFERENCE_LIST.has(name)) for (const part of value.split(/\\s+/)) take(part);\n else if (REFERENCE.has(name) || name === 'href' || name === 'xlink:href') take(value);\n }\n for (const value of Object.values(node.style)) for (const found of urlAliases(value)) take(found);\n for (const value of Object.values(node.tokens ?? {})) for (const found of urlAliases(value)) take(found);\n for (const child of node.children) if (holds(boundary, child)) walk(child);\n };\n\n walk(boundary.node);\n\n // Unmapped values pass through unchanged. A reference this walk never reached\n // — one inside a nested boundary, reaching back out — is left as the subject\n // alias it already was, which is visible and wrong in the same direction as\n // doing nothing, rather than silently colliding with a local slot.\n const rewrite = (value: string): string => local.get(value) ?? value;\n\n return {\n alias: rewrite,\n style: (value) =>\n value.replace(/url\\(\\s*(['\"]?)#([^'\")]+)\\1\\s*\\)/g, (match, quote: string, id: string) => {\n const mapped = local.get(`#${id}`);\n return mapped === undefined ? match : `url(${quote}${mapped}${quote})`;\n }),\n attribute: (name, value) => {\n if (REFERENCE_LIST.has(name)) {\n return value\n .split(/\\s+/)\n .map((part) => rewrite(part))\n .join(' ');\n }\n if (REFERENCE.has(name) || name === 'href' || name === 'xlink:href') return rewrite(value);\n return value;\n },\n };\n}\n\nfunction urlAliases(value: string): readonly string[] {\n return [...value.matchAll(/url\\(\\s*['\"]?#([^'\")]+)['\"]?\\s*\\)/g)].map((match) => `#${match[1]}`);\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import type { NodePath, SemanticNode } from '../format/snapshot.js';
2
+ export interface LocationStep {
3
+ /** `landmark`, `named`, or `ordinal` — why this step is worth mentioning. */
4
+ readonly kind: 'landmark' | 'named' | 'ordinal';
5
+ readonly text: string;
6
+ }
7
+ export interface Location {
8
+ readonly steps: readonly LocationStep[];
9
+ /** The nearest landmark, for grouping. Spec §6.3's "right panel". */
10
+ readonly region?: string;
11
+ /** One phrase: `main → region "Todos" → item 2 of 3`. */
12
+ readonly where: string;
13
+ }
14
+ /**
15
+ * Describe where `path` sits, from the landmarks and names above it.
16
+ *
17
+ * Returns an empty location rather than throwing when the path does not resolve.
18
+ * A stale or unresolvable path is a defect somewhere upstream, and losing the
19
+ * whole report over it would hide the finding that actually matters.
20
+ */
21
+ export declare function locate(root: SemanticNode, path: NodePath): Location;
22
+ //# sourceMappingURL=locate.d.ts.map
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Orientation: *where* a change is, in words a person can act on.
3
+ *
4
+ * A path like `0/0/0/2/1/0` is an address, not a location. It tells a reviewer
5
+ * nothing, and it tells an agent nothing either — which is how a report ends up
6
+ * being read as "looks right, merge".
7
+ *
8
+ * Coordinates are no better. Spec §6.3 is explicit that spatial location is
9
+ * expressed through landmarks rather than pixels, and the reason is that
10
+ * coordinates are the least stable thing about a page: a rect moves whenever
11
+ * anything above it reflows, so "at (412, 880)" describes this build and no
12
+ * other. A landmark path describes the *page*, and stays true while the layout
13
+ * moves under it.
14
+ *
15
+ * So a location is built from the things a user would use to describe where they
16
+ * are: the landmark they are in, the named region or dialog, and their position
17
+ * in a list. `main → region "Todos" → list → item 2 of 3` is a sentence someone
18
+ * can follow without opening a screenshot.
19
+ */
20
+ /** Roles that orient: a user could say "in the navigation" and be understood. */
21
+ const LANDMARKS = new Set([
22
+ 'banner', 'navigation', 'main', 'complementary', 'contentinfo',
23
+ 'region', 'form', 'search', 'dialog', 'alertdialog',
24
+ ]);
25
+ /** Roles whose children are positional, so an ordinal is worth reporting. */
26
+ const ORDINAL_CONTAINERS = new Set(['list', 'table', 'rowgroup', 'row', 'tablist', 'menu', 'grid']);
27
+ /**
28
+ * Describe where `path` sits, from the landmarks and names above it.
29
+ *
30
+ * Returns an empty location rather than throwing when the path does not resolve.
31
+ * A stale or unresolvable path is a defect somewhere upstream, and losing the
32
+ * whole report over it would hide the finding that actually matters.
33
+ */
34
+ export function locate(root, path) {
35
+ const chain = walkTo(root, path);
36
+ if (chain.length === 0)
37
+ return { steps: [], where: '' };
38
+ const steps = [];
39
+ let region;
40
+ for (const [index, node] of chain.entries()) {
41
+ const child = chain[index + 1];
42
+ if (node.role !== undefined && LANDMARKS.has(node.role)) {
43
+ const text = node.name !== undefined ? `${node.role} "${node.name}"` : node.role;
44
+ steps.push({ kind: 'landmark', text });
45
+ // Nearest wins: the innermost landmark is the one a person would name.
46
+ region = text;
47
+ continue;
48
+ }
49
+ // A named non-landmark still orients — `tabpanel "Active"`, `group "Filters"`.
50
+ if (node.role !== undefined && node.name !== undefined && index > 0) {
51
+ steps.push({ kind: 'named', text: `${node.role} "${node.name}"` });
52
+ continue;
53
+ }
54
+ if (child !== undefined && node.role !== undefined && ORDINAL_CONTAINERS.has(node.role)) {
55
+ const position = node.children.indexOf(child);
56
+ if (position >= 0) {
57
+ steps.push({
58
+ kind: 'ordinal',
59
+ text: `${node.role} item ${position + 1} of ${node.children.length}`,
60
+ });
61
+ }
62
+ }
63
+ }
64
+ return {
65
+ steps,
66
+ ...(region !== undefined ? { region } : {}),
67
+ // Deepest first would read backwards; a location is spoken outside-in, the
68
+ // way someone points at a screen.
69
+ where: steps.map((step) => step.text).join(' → '),
70
+ };
71
+ }
72
+ /**
73
+ * Nodes from the root down to `path`, inclusive.
74
+ *
75
+ * Paths are child indices, so this is index navigation rather than a search —
76
+ * except for portal segments, which are keyed rather than positional because a
77
+ * portalled subtree is appended outside the DOM child order (ADR-0007).
78
+ */
79
+ function walkTo(root, path) {
80
+ const segments = path.split('/');
81
+ if (segments.length === 0)
82
+ return [];
83
+ const chain = [root];
84
+ let current = root;
85
+ for (const segment of segments.slice(1)) {
86
+ // `portal:N` names the *N-th* portal, not "a portal". Taking the first meant
87
+ // every node in a second or later portalled subtree was located through the
88
+ // wrong one, and produced a confident landmark phrase about somewhere else —
89
+ // a subject with two dialogs open is the ordinary case.
90
+ const next = segment.startsWith('portal:')
91
+ ? current.children.filter((child) => child.portalled === true)[Number.parseInt(segment.slice('portal:'.length), 10)]
92
+ : current.children[Number.parseInt(segment, 10)];
93
+ if (next === undefined)
94
+ return chain;
95
+ chain.push(next);
96
+ current = next;
97
+ }
98
+ return chain;
99
+ }
100
+ //# sourceMappingURL=locate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locate.js","sourceRoot":"","sources":["../../src/attribute/locate.ts"],"names":[],"mappings":"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","sourcesContent":["import type { NodePath, SemanticNode } from '../format/snapshot.js';\n\n/**\n * Orientation: *where* a change is, in words a person can act on.\n *\n * A path like `0/0/0/2/1/0` is an address, not a location. It tells a reviewer\n * nothing, and it tells an agent nothing either — which is how a report ends up\n * being read as \"looks right, merge\".\n *\n * Coordinates are no better. Spec §6.3 is explicit that spatial location is\n * expressed through landmarks rather than pixels, and the reason is that\n * coordinates are the least stable thing about a page: a rect moves whenever\n * anything above it reflows, so \"at (412, 880)\" describes this build and no\n * other. A landmark path describes the *page*, and stays true while the layout\n * moves under it.\n *\n * So a location is built from the things a user would use to describe where they\n * are: the landmark they are in, the named region or dialog, and their position\n * in a list. `main → region \"Todos\" → list → item 2 of 3` is a sentence someone\n * can follow without opening a screenshot.\n */\n\n/** Roles that orient: a user could say \"in the navigation\" and be understood. */\nconst LANDMARKS = new Set([\n 'banner', 'navigation', 'main', 'complementary', 'contentinfo',\n 'region', 'form', 'search', 'dialog', 'alertdialog',\n]);\n\n/** Roles whose children are positional, so an ordinal is worth reporting. */\nconst ORDINAL_CONTAINERS = new Set(['list', 'table', 'rowgroup', 'row', 'tablist', 'menu', 'grid']);\n\nexport interface LocationStep {\n /** `landmark`, `named`, or `ordinal` — why this step is worth mentioning. */\n readonly kind: 'landmark' | 'named' | 'ordinal';\n readonly text: string;\n}\n\nexport interface Location {\n readonly steps: readonly LocationStep[];\n /** The nearest landmark, for grouping. Spec §6.3's \"right panel\". */\n readonly region?: string;\n /** One phrase: `main → region \"Todos\" → item 2 of 3`. */\n readonly where: string;\n}\n\n/**\n * Describe where `path` sits, from the landmarks and names above it.\n *\n * Returns an empty location rather than throwing when the path does not resolve.\n * A stale or unresolvable path is a defect somewhere upstream, and losing the\n * whole report over it would hide the finding that actually matters.\n */\nexport function locate(root: SemanticNode, path: NodePath): Location {\n const chain = walkTo(root, path);\n if (chain.length === 0) return { steps: [], where: '' };\n\n const steps: LocationStep[] = [];\n let region: string | undefined;\n\n for (const [index, node] of chain.entries()) {\n const child = chain[index + 1];\n\n if (node.role !== undefined && LANDMARKS.has(node.role)) {\n const text = node.name !== undefined ? `${node.role} \"${node.name}\"` : node.role;\n steps.push({ kind: 'landmark', text });\n // Nearest wins: the innermost landmark is the one a person would name.\n region = text;\n continue;\n }\n\n // A named non-landmark still orients — `tabpanel \"Active\"`, `group \"Filters\"`.\n if (node.role !== undefined && node.name !== undefined && index > 0) {\n steps.push({ kind: 'named', text: `${node.role} \"${node.name}\"` });\n continue;\n }\n\n if (child !== undefined && node.role !== undefined && ORDINAL_CONTAINERS.has(node.role)) {\n const position = node.children.indexOf(child);\n if (position >= 0) {\n steps.push({\n kind: 'ordinal',\n text: `${node.role} item ${position + 1} of ${node.children.length}`,\n });\n }\n }\n }\n\n return {\n steps,\n ...(region !== undefined ? { region } : {}),\n // Deepest first would read backwards; a location is spoken outside-in, the\n // way someone points at a screen.\n where: steps.map((step) => step.text).join(' → '),\n };\n}\n\n/**\n * Nodes from the root down to `path`, inclusive.\n *\n * Paths are child indices, so this is index navigation rather than a search —\n * except for portal segments, which are keyed rather than positional because a\n * portalled subtree is appended outside the DOM child order (ADR-0007).\n */\nfunction walkTo(root: SemanticNode, path: NodePath): readonly SemanticNode[] {\n const segments = path.split('/');\n if (segments.length === 0) return [];\n\n const chain: SemanticNode[] = [root];\n let current = root;\n\n for (const segment of segments.slice(1)) {\n // `portal:N` names the *N-th* portal, not \"a portal\". Taking the first meant\n // every node in a second or later portalled subtree was located through the\n // wrong one, and produced a confident landmark phrase about somewhere else —\n // a subject with two dialogs open is the ordinary case.\n const next = segment.startsWith('portal:')\n ? current.children.filter((child) => child.portalled === true)[\n Number.parseInt(segment.slice('portal:'.length), 10)\n ]\n : current.children[Number.parseInt(segment, 10)];\n\n if (next === undefined) return chain;\n chain.push(next);\n current = next;\n }\n\n return chain;\n}\n"]}
@@ -0,0 +1,156 @@
1
+ import type { Rect } from '../format/capture.js';
2
+ import type { SemanticSnapshot } from '../format/snapshot.js';
3
+ /**
4
+ * A change as a bitmask: where on the canvas, and what was excluded from it.
5
+ *
6
+ * Arithmetic, and nothing else. No DOM, no snapshot, no notion of what a
7
+ * component is — which is why it is the half of attribution that can be tested
8
+ * with a hand-written mask and why it is a file of its own. Everything here
9
+ * answers *where on the canvas*; `region.ts` joins those coordinates to a tree
10
+ * and answers *what is there*, and the two fail differently enough that reading
11
+ * one to understand the other was how they came to share 500 lines.
12
+ *
13
+ * The pixel count is what this exists to defeat. "5482 pixels changed" cannot be
14
+ * read, cannot be assigned, and leaves opening the image as the only available
15
+ * response — the expensive act the tool was supposed to replace.
16
+ */
17
+ /**
18
+ * Per-pixel changed/unchanged, row-major, one byte per pixel.
19
+ *
20
+ * A mask rather than a diff image: an image is for looking at, and everything
21
+ * downstream of here wants to compute. Producing one is the comparison phase's
22
+ * job and involves PNG decoding; consuming one is pure.
23
+ */
24
+ export interface ChangeMask {
25
+ readonly width: number;
26
+ readonly height: number;
27
+ /** `1` where the pixel differs. Length is `width * height`. */
28
+ readonly data: Uint8Array;
29
+ /** Count of set bytes, carried so callers need not rescan. */
30
+ readonly changed: number;
31
+ }
32
+ export interface DiffRegion extends Rect {
33
+ /** Changed pixels inside the box. Always ≤ `width * height`. */
34
+ readonly pixels: number;
35
+ /** `pixels / (width * height)`. Low means scattered; high means a solid block. */
36
+ readonly density: number;
37
+ }
38
+ export interface IsolationOptions {
39
+ /**
40
+ * Grid size, in pixels, at which neighbouring changes are considered one place.
41
+ *
42
+ * Not a tuning knob so much as a statement about what a region *is*. At cell 1
43
+ * every antialiased glyph edge is its own region and a paragraph of restyled
44
+ * text produces four hundred of them, which is the same unreadable output as a
45
+ * single number, only longer. At cell 8 a word is one region and a button is
46
+ * one region, which is the granularity a person names when they point at a
47
+ * screen.
48
+ */
49
+ readonly cell?: number;
50
+ /**
51
+ * Cap on regions returned, largest first.
52
+ *
53
+ * Truncation is reported in {@link Isolation.truncated} rather than applied
54
+ * silently. A capped list that does not say it was capped reads as complete
55
+ * coverage, and the reader has no way to know the difference.
56
+ */
57
+ readonly limit?: number;
58
+ }
59
+ export interface Isolation {
60
+ readonly regions: readonly DiffRegion[];
61
+ /** Regions found but not returned, because of `limit`. `0` in the normal case. */
62
+ readonly truncated: number;
63
+ /** Changed pixels in the truncated tail. Nothing is lost silently. */
64
+ readonly truncatedPixels: number;
65
+ }
66
+ /**
67
+ * Cluster a change mask into regions.
68
+ *
69
+ * Connected components are computed on a coarse grid rather than on the pixels
70
+ * themselves. That is a performance decision and a semantic one at once: the
71
+ * coarse pass is one linear sweep instead of a merge over thousands of glyph-edge
72
+ * fragments, and it produces the grouping a reader would have produced by eye.
73
+ * Bounding boxes are then tightened back onto the actual changed pixels, so a
74
+ * region's coordinates are exact even though its *membership* was decided coarsely.
75
+ */
76
+ export declare function isolateRegions(mask: ChangeMask, options?: IsolationOptions): Isolation;
77
+ export interface Subtraction {
78
+ /** The mask with the excluded boxes cleared. Never the same object. */
79
+ readonly mask: ChangeMask;
80
+ /** Changed pixels that fell inside an excluded box. */
81
+ readonly ignored: number;
82
+ /**
83
+ * Pixels each input box cleared, in the order the boxes were given.
84
+ *
85
+ * Positional rather than keyed, because this file knows nothing about ignore
86
+ * rules and should not start: a box is geometry. The caller that supplied the
87
+ * boxes knows which rule each came from and can attribute the counts, which is
88
+ * what a per-rule register is assembled from.
89
+ *
90
+ * Two boxes overlapping share their pixels on a first-come basis, so these sum
91
+ * to {@link Subtraction.ignored} exactly and never double-count.
92
+ */
93
+ readonly cleared: readonly number[];
94
+ /**
95
+ * Boxes that covered no changed pixel at all.
96
+ *
97
+ * The raster half of the dead-ignore register. A mask drawn around a carousel
98
+ * that has since been removed covers nothing, and an operator who cannot see
99
+ * that keeps a hole in the suite because deleting it feels risky.
100
+ *
101
+ * Judged against the *original* mask, not against what earlier boxes left. Two
102
+ * rules covering one element is ordinary — a selector and a marker attribute
103
+ * reaching the same node — and clearing is first-come, so the second box would
104
+ * otherwise clear zero and be reported as covering nothing at all. That reads
105
+ * as "delete this rule" about a rule that is working.
106
+ */
107
+ readonly inert: readonly Rect[];
108
+ }
109
+ /**
110
+ * Clear excluded boxes out of a change mask, and say what that cost.
111
+ *
112
+ * The raster half of an ignore (spec 0024). The semantic half drops deltas under
113
+ * a subtree; this drops the pixels the same subtree occupied, so the two tiers
114
+ * cannot disagree about what the subject is — a region excluded semantically and
115
+ * still compared on pixels arrives as `unexplained`, the highest severity in the
116
+ * system, for something the operator already said was not the subject.
117
+ *
118
+ * Subtracting *before* isolation rather than filtering regions afterwards, which
119
+ * looks equivalent and is not: a region that straddles the boundary would
120
+ * otherwise be dropped whole or kept whole, and both answers are wrong. Clearing
121
+ * pixels lets the part outside the box cluster on its own and be reported.
122
+ *
123
+ * The count is returned rather than folded away. `changed` on the result is what
124
+ * the run compares against zero; `ignored` is what it owes the reader, because a
125
+ * comparison that discarded four thousand pixels and reported "no difference" is
126
+ * the failure this whole mechanism is written around.
127
+ */
128
+ export declare function subtractRegions(mask: ChangeMask, boxes: readonly Rect[]): Subtraction;
129
+ /**
130
+ * Excluded subtrees as device-pixel boxes on the raster's own canvas.
131
+ *
132
+ * The conversion that lets one declaration serve both tiers (spec 0024). A site
133
+ * is recorded in CSS pixels in page space, because that is what a document knows;
134
+ * a mask is device pixels in raster space, because that is what a screenshot is.
135
+ * Getting the transform wrong is not a small error — at the wrong scale every box
136
+ * lands in the top-left quadrant and silences whatever happens to be there — so
137
+ * it is inverted from exactly the expression `attributeRegions` uses in the other
138
+ * direction, in one place, rather than re-derived per caller.
139
+ *
140
+ * Sites with no `rect` are dropped: a profile with no layout engine observed no
141
+ * box, and a box that was never observed must never be inferred. The semantic
142
+ * half of the same rule still applies on that profile, which is the correct
143
+ * asymmetry — that profile decides no pixels either.
144
+ */
145
+ export declare function excludedBoxes(snapshot: SemanticSnapshot, options: {
146
+ readonly scale: number;
147
+ readonly origin?: {
148
+ readonly x: number;
149
+ readonly y: number;
150
+ };
151
+ }): readonly ExcludedBox[];
152
+ /** A box, carrying the rule that excluded it so absorption can be attributed. */
153
+ export interface ExcludedBox extends Rect {
154
+ readonly rule: string;
155
+ }
156
+ //# sourceMappingURL=mask.d.ts.map