@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
@@ -6,7 +6,8 @@ import type { SemanticNode } from '../../format/snapshot.js';
6
6
  * Diffing by path alone would report a single insertion as "every following node
7
7
  * changed" — the classic snapshot-test failure that trains people to stop
8
8
  * reading diffs. Matching therefore happens by identity first, content second,
9
- * and position last.
9
+ * and position last; a node no key relates is paired only by the author that
10
+ * recorded writing it.
10
11
  *
11
12
  * The middle pass is the one that took three attempts to find. Identity keys
12
13
  * collapse whenever a node has no alias, no accessible name and no provenance —
@@ -82,6 +82,25 @@ export function matchTrees(baseline, candidate) {
82
82
  if (claimed !== undefined)
83
83
  partner[afterIndex] = claimed;
84
84
  });
85
+ // Pass 3 — a replacement its author recorded. A component that swaps its
86
+ // own output, `Toggle` rendering a `<div>` where it rendered a checkbox,
87
+ // leaves two nodes no key relates: they share no content, and one is a
88
+ // named role where the other is a bare tag. What relates them is who wrote
89
+ // them. Under one matched parent, one node left and one arrived, and the
90
+ // same component created both, so the second is read as what that
91
+ // component rendered instead of the first. The record cannot tell that from
92
+ // an element dropped and an unrelated one drawn elsewhere in the row, and
93
+ // the pair's position is not asked: the deltas then say what the slot
94
+ // became and that it moved.
95
+ //
96
+ // Exactly one and one, or nothing. Two that left and two that arrived from
97
+ // one author could pair either way, and choosing by position is the guess
98
+ // pass 2 already declines across keys. And never two nodes that each
99
+ // declare who they are: `createdBy` names a component, and every `<Button>`
100
+ // is created by `Button`, so a Save button and an Export link under one
101
+ // toolbar share an author while their names, which pass 2 has read, say
102
+ // they are two elements.
103
+ replacements(before.children, after.children, taken, partner);
85
104
  const sequence = [];
86
105
  after.children.forEach((child, afterIndex) => {
87
106
  const beforeIndex = partner[afterIndex];
@@ -105,6 +124,39 @@ export function matchTrees(baseline, candidate) {
105
124
  });
106
125
  }
107
126
  }
127
+ /** Pairs, in place, the one unclaimed node on each side that one author created. */
128
+ function replacements(before, after, taken, partner) {
129
+ const left = byAuthor(before, (position) => !taken.has(position));
130
+ const arrived = byAuthor(after, (position) => partner[position] === undefined);
131
+ for (const [author, [beforeIndex, ...more]] of left) {
132
+ const counterpart = arrived.get(author);
133
+ if (more.length > 0 || counterpart?.length !== 1)
134
+ continue;
135
+ if (declared(before[beforeIndex]) && declared(after[counterpart[0]]))
136
+ continue;
137
+ taken.add(beforeIndex);
138
+ partner[counterpart[0]] = beforeIndex;
139
+ }
140
+ }
141
+ /** Whether a node says who it is, by the first two of `matchKey`'s keys. */
142
+ function declared(node) {
143
+ return node.alias !== undefined || (node.role !== undefined && node.name !== undefined);
144
+ }
145
+ /** Positions of the nodes `open` admits, by the component that created each. */
146
+ function byAuthor(children, open) {
147
+ const authors = new Map();
148
+ children.forEach((child, position) => {
149
+ const author = child.provenance?.createdBy;
150
+ if (author === undefined || !open(position))
151
+ return;
152
+ const positions = authors.get(author);
153
+ if (positions)
154
+ positions.push(position);
155
+ else
156
+ authors.set(author, [position]);
157
+ });
158
+ return authors;
159
+ }
108
160
  function index(children, key) {
109
161
  const buckets = new Map();
110
162
  children.forEach((child, position) => {
@@ -1 +1 @@
1
- {"version":3,"file":"match.js","sourceRoot":"","sources":["../../../src/compare/diff/match.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SemanticNode } from '../../format/snapshot.js';\n\n/**\n * Node correspondence.\n *\n * Paths are positional, so inserting one sibling renumbers everything after it.\n * Diffing by path alone would report a single insertion as \"every following node\n * changed\" — the classic snapshot-test failure that trains people to stop\n * reading diffs. Matching therefore happens by identity first, content second,\n * and position last.\n *\n * The middle pass is the one that took three attempts to find. Identity keys\n * collapse whenever a node has no alias, no accessible name and no provenance —\n * five bare `<li>`s bucket together as `tag:li` — and pairing inside that bucket\n * by position turns a rotation into \"every item's text changed\". The corpus case\n * `reorder/list` was written to catch exactly that and could not: its assertions\n * were the verdict and the root count, and both are correct while the report is\n * useless. Scoring the *band* is what exposed it — five `text-changed` deltas\n * band as `content` where a rotation bands as `geometry`.\n */\n\nexport interface Matching {\n /** Baseline node → candidate node. */\n readonly pairs: ReadonlyMap<SemanticNode, SemanticNode>;\n readonly added: readonly SemanticNode[];\n readonly removed: readonly SemanticNode[];\n /**\n * Pairs whose position among their siblings changed *relative to each other*.\n *\n * Not \"whose index differs\". Prepending one item shifts every index after it\n * without reordering anything, and reporting three moves for one insertion is\n * the same fatigue that positional diffing produces. The moved set is the\n * complement of the longest run that stayed in order, which is the smallest\n * set of nodes whose movement explains the new sequence.\n */\n readonly moved: ReadonlySet<SemanticNode>;\n}\n\n/**\n * Identity key, most specific first.\n *\n * Ordering matters: a structural alias is the strongest signal available because\n * it survives reordering and is stable across renders (that being the point of\n * ADR-0003's aliasing). Role plus accessible name is next, since it is what a\n * user of assistive technology would call the node. Owner chain plus tag comes\n * third — two `<span>`s rendered by the same component are interchangeable in a\n * way two spans from different components are not. Bare tag is the last resort.\n */\nfunction matchKey(node: SemanticNode): string {\n if (node.alias !== undefined) return `alias:${node.alias}`;\n if (node.role !== undefined && node.name !== undefined) {\n return `aria:${node.role}|${node.name}`;\n }\n if (node.provenance && node.provenance.owners.length > 0) {\n const chain = node.provenance.owners.map((owner) => owner.name).join('>');\n return `own:${chain}:${node.tag}`;\n }\n return `tag:${node.tag}`;\n}\n\nconst SIGNATURES = new WeakMap<SemanticNode, string>();\n\n/**\n * What this subtree *says*, ignoring where it sits.\n *\n * Deliberately narrow: tag, the three ARIA fields, text, and the same of every\n * descendant. Style is excluded, because a node that was restyled and a node\n * that was replaced must not be confused — pairing by appearance would let a\n * recolour look like a substitution. Position is excluded because that is the\n * whole point.\n *\n * Cached per node: the recursion is O(subtree) and a parent computing it for\n * every child at every level would be quadratic in depth.\n */\nfunction contentSignature(node: SemanticNode): string {\n const cached = SIGNATURES.get(node);\n if (cached !== undefined) return cached;\n\n const own = [node.tag, node.role ?? '', node.name ?? '', node.description ?? '', node.text ?? ''];\n const signature = `${own.join('\u0001')}(${node.children.map(contentSignature).join('\u0002')})`;\n\n SIGNATURES.set(node, signature);\n return signature;\n}\n\n/**\n * Content pass key. Scoped by identity so the refinement never crosses a\n * boundary the identity key drew — two components rendering the same string are\n * not the same node however identical their subtrees.\n */\nfunction contentKey(node: SemanticNode): string {\n return `${matchKey(node)}\\u0000${contentSignature(node)}`;\n}\n\nexport function matchTrees(baseline: SemanticNode, candidate: SemanticNode): Matching {\n const pairs = new Map<SemanticNode, SemanticNode>();\n const added: SemanticNode[] = [];\n const removed: SemanticNode[] = [];\n const moved = new Set<SemanticNode>();\n\n pairs.set(baseline, candidate);\n matchChildren(baseline, candidate);\n\n return { pairs, added, removed, moved };\n\n function matchChildren(before: SemanticNode, after: SemanticNode): void {\n const byContent = index(before.children, contentKey);\n const byIdentity = index(before.children, matchKey);\n const taken = new Set<number>();\n\n /** Baseline index each candidate paired with, indexed by candidate position. */\n const partner: (number | undefined)[] = Array.from(\n { length: after.children.length },\n () => undefined,\n );\n\n // Pass 1 — identical content. A node that says the same thing is the same\n // node, wherever it moved to.\n after.children.forEach((child, afterIndex) => {\n const claimed = claim(byContent.get(contentKey(child)), taken);\n if (claimed !== undefined) partner[afterIndex] = claimed;\n });\n\n // Pass 2 — whatever is left, by identity and then by order of appearance.\n // This is what turns `[a, b, c]` → `[a, x, c]` into one changed string\n // rather than one removal and one addition: `b` and `x` are the only two\n // nodes their bucket has left, so they are each other's counterpart.\n after.children.forEach((child, afterIndex) => {\n if (partner[afterIndex] !== undefined) return;\n const claimed = claim(byIdentity.get(matchKey(child)), taken);\n if (claimed !== undefined) partner[afterIndex] = claimed;\n });\n\n const sequence: number[] = [];\n after.children.forEach((child, afterIndex) => {\n const beforeIndex = partner[afterIndex];\n if (beforeIndex === undefined) {\n // Unmatched candidates are recorded whole. Their descendants are not\n // walked: a new subtree is one change to review, not one per node in it.\n added.push(child);\n return;\n }\n\n sequence.push(beforeIndex);\n const counterpart = before.children[beforeIndex]!;\n pairs.set(counterpart, child);\n matchChildren(counterpart, child);\n });\n\n for (const beforeIndex of outOfOrder(sequence)) {\n moved.add(before.children[beforeIndex]!);\n }\n\n before.children.forEach((child, index) => {\n if (!taken.has(index)) removed.push(child);\n });\n }\n}\n\nfunction index(\n children: readonly SemanticNode[],\n key: (node: SemanticNode) => string,\n): Map<string, number[]> {\n const buckets = new Map<string, number[]>();\n\n children.forEach((child, position) => {\n const bucket = buckets.get(key(child));\n if (bucket) bucket.push(position);\n else buckets.set(key(child), [position]);\n });\n\n return buckets;\n}\n\n/** Lowest unclaimed index in a bucket, marking it claimed. */\nfunction claim(bucket: number[] | undefined, taken: Set<number>): number | undefined {\n const free = bucket?.find((position) => !taken.has(position));\n if (free !== undefined) taken.add(free);\n return free;\n}\n\n/**\n * Indices that must have moved for `sequence` to be in this order.\n *\n * The complement of a longest increasing subsequence: everything in that\n * subsequence kept its relative order and needs no explanation, and what is left\n * is the smallest set of movements that produces the observed sequence. A\n * prepend leaves the sequence increasing and therefore reports nothing moved,\n * which is the answer `prepend/list` asks for — one added node, and not three\n * displaced siblings alongside it.\n */\nfunction outOfOrder(sequence: readonly number[]): readonly number[] {\n if (sequence.length < 2) return [];\n\n // Patience sorting. `tails[k]` is the smallest tail of an increasing run of\n // length k+1; `previous` reconstructs which elements formed the longest one.\n const tails: number[] = [];\n const tailIndex: number[] = [];\n const previous: (number | undefined)[] = Array.from(\n { length: sequence.length },\n () => undefined,\n );\n\n for (let position = 0; position < sequence.length; position += 1) {\n const value = sequence[position]!;\n\n let low = 0;\n let high = tails.length;\n while (low < high) {\n const mid = (low + high) >> 1;\n if (tails[mid]! < value) low = mid + 1;\n else high = mid;\n }\n\n tails[low] = value;\n tailIndex[low] = position;\n previous[position] = low > 0 ? tailIndex[low - 1] : undefined;\n }\n\n const kept = new Set<number>();\n let cursor: number | undefined = tailIndex[tails.length - 1];\n while (cursor !== undefined) {\n kept.add(cursor);\n cursor = previous[cursor];\n }\n\n return sequence.filter((_, position) => !kept.has(position));\n}\n"]}
1
+ {"version":3,"file":"match.js","sourceRoot":"","sources":["../../../src/compare/diff/match.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SemanticNode } from '../../format/snapshot.js';\n\n/**\n * Node correspondence.\n *\n * Paths are positional, so inserting one sibling renumbers everything after it.\n * Diffing by path alone would report a single insertion as \"every following node\n * changed\" — the classic snapshot-test failure that trains people to stop\n * reading diffs. Matching therefore happens by identity first, content second,\n * and position last; a node no key relates is paired only by the author that\n * recorded writing it.\n *\n * The middle pass is the one that took three attempts to find. Identity keys\n * collapse whenever a node has no alias, no accessible name and no provenance —\n * five bare `<li>`s bucket together as `tag:li` — and pairing inside that bucket\n * by position turns a rotation into \"every item's text changed\". The corpus case\n * `reorder/list` was written to catch exactly that and could not: its assertions\n * were the verdict and the root count, and both are correct while the report is\n * useless. Scoring the *band* is what exposed it — five `text-changed` deltas\n * band as `content` where a rotation bands as `geometry`.\n */\n\nexport interface Matching {\n /** Baseline node → candidate node. */\n readonly pairs: ReadonlyMap<SemanticNode, SemanticNode>;\n readonly added: readonly SemanticNode[];\n readonly removed: readonly SemanticNode[];\n /**\n * Pairs whose position among their siblings changed *relative to each other*.\n *\n * Not \"whose index differs\". Prepending one item shifts every index after it\n * without reordering anything, and reporting three moves for one insertion is\n * the same fatigue that positional diffing produces. The moved set is the\n * complement of the longest run that stayed in order, which is the smallest\n * set of nodes whose movement explains the new sequence.\n */\n readonly moved: ReadonlySet<SemanticNode>;\n}\n\n/**\n * Identity key, most specific first.\n *\n * Ordering matters: a structural alias is the strongest signal available because\n * it survives reordering and is stable across renders (that being the point of\n * ADR-0003's aliasing). Role plus accessible name is next, since it is what a\n * user of assistive technology would call the node. Owner chain plus tag comes\n * third — two `<span>`s rendered by the same component are interchangeable in a\n * way two spans from different components are not. Bare tag is the last resort.\n */\nfunction matchKey(node: SemanticNode): string {\n if (node.alias !== undefined) return `alias:${node.alias}`;\n if (node.role !== undefined && node.name !== undefined) {\n return `aria:${node.role}|${node.name}`;\n }\n if (node.provenance && node.provenance.owners.length > 0) {\n const chain = node.provenance.owners.map((owner) => owner.name).join('>');\n return `own:${chain}:${node.tag}`;\n }\n return `tag:${node.tag}`;\n}\n\nconst SIGNATURES = new WeakMap<SemanticNode, string>();\n\n/**\n * What this subtree *says*, ignoring where it sits.\n *\n * Deliberately narrow: tag, the three ARIA fields, text, and the same of every\n * descendant. Style is excluded, because a node that was restyled and a node\n * that was replaced must not be confused — pairing by appearance would let a\n * recolour look like a substitution. Position is excluded because that is the\n * whole point.\n *\n * Cached per node: the recursion is O(subtree) and a parent computing it for\n * every child at every level would be quadratic in depth.\n */\nfunction contentSignature(node: SemanticNode): string {\n const cached = SIGNATURES.get(node);\n if (cached !== undefined) return cached;\n\n const own = [node.tag, node.role ?? '', node.name ?? '', node.description ?? '', node.text ?? ''];\n const signature = `${own.join('\u0001')}(${node.children.map(contentSignature).join('\u0002')})`;\n\n SIGNATURES.set(node, signature);\n return signature;\n}\n\n/**\n * Content pass key. Scoped by identity so the refinement never crosses a\n * boundary the identity key drew — two components rendering the same string are\n * not the same node however identical their subtrees.\n */\nfunction contentKey(node: SemanticNode): string {\n return `${matchKey(node)}\\u0000${contentSignature(node)}`;\n}\n\nexport function matchTrees(baseline: SemanticNode, candidate: SemanticNode): Matching {\n const pairs = new Map<SemanticNode, SemanticNode>();\n const added: SemanticNode[] = [];\n const removed: SemanticNode[] = [];\n const moved = new Set<SemanticNode>();\n\n pairs.set(baseline, candidate);\n matchChildren(baseline, candidate);\n\n return { pairs, added, removed, moved };\n\n function matchChildren(before: SemanticNode, after: SemanticNode): void {\n const byContent = index(before.children, contentKey);\n const byIdentity = index(before.children, matchKey);\n const taken = new Set<number>();\n\n /** Baseline index each candidate paired with, indexed by candidate position. */\n const partner: (number | undefined)[] = Array.from(\n { length: after.children.length },\n () => undefined,\n );\n\n // Pass 1 — identical content. A node that says the same thing is the same\n // node, wherever it moved to.\n after.children.forEach((child, afterIndex) => {\n const claimed = claim(byContent.get(contentKey(child)), taken);\n if (claimed !== undefined) partner[afterIndex] = claimed;\n });\n\n // Pass 2 — whatever is left, by identity and then by order of appearance.\n // This is what turns `[a, b, c]` → `[a, x, c]` into one changed string\n // rather than one removal and one addition: `b` and `x` are the only two\n // nodes their bucket has left, so they are each other's counterpart.\n after.children.forEach((child, afterIndex) => {\n if (partner[afterIndex] !== undefined) return;\n const claimed = claim(byIdentity.get(matchKey(child)), taken);\n if (claimed !== undefined) partner[afterIndex] = claimed;\n });\n\n // Pass 3 — a replacement its author recorded. A component that swaps its\n // own output, `Toggle` rendering a `<div>` where it rendered a checkbox,\n // leaves two nodes no key relates: they share no content, and one is a\n // named role where the other is a bare tag. What relates them is who wrote\n // them. Under one matched parent, one node left and one arrived, and the\n // same component created both, so the second is read as what that\n // component rendered instead of the first. The record cannot tell that from\n // an element dropped and an unrelated one drawn elsewhere in the row, and\n // the pair's position is not asked: the deltas then say what the slot\n // became and that it moved.\n //\n // Exactly one and one, or nothing. Two that left and two that arrived from\n // one author could pair either way, and choosing by position is the guess\n // pass 2 already declines across keys. And never two nodes that each\n // declare who they are: `createdBy` names a component, and every `<Button>`\n // is created by `Button`, so a Save button and an Export link under one\n // toolbar share an author while their names, which pass 2 has read, say\n // they are two elements.\n replacements(before.children, after.children, taken, partner);\n\n const sequence: number[] = [];\n after.children.forEach((child, afterIndex) => {\n const beforeIndex = partner[afterIndex];\n if (beforeIndex === undefined) {\n // Unmatched candidates are recorded whole. Their descendants are not\n // walked: a new subtree is one change to review, not one per node in it.\n added.push(child);\n return;\n }\n\n sequence.push(beforeIndex);\n const counterpart = before.children[beforeIndex]!;\n pairs.set(counterpart, child);\n matchChildren(counterpart, child);\n });\n\n for (const beforeIndex of outOfOrder(sequence)) {\n moved.add(before.children[beforeIndex]!);\n }\n\n before.children.forEach((child, index) => {\n if (!taken.has(index)) removed.push(child);\n });\n }\n}\n\n/** Pairs, in place, the one unclaimed node on each side that one author created. */\nfunction replacements(\n before: readonly SemanticNode[],\n after: readonly SemanticNode[],\n taken: Set<number>,\n partner: (number | undefined)[],\n): void {\n const left = byAuthor(before, (position) => !taken.has(position));\n const arrived = byAuthor(after, (position) => partner[position] === undefined);\n\n for (const [author, [beforeIndex, ...more]] of left) {\n const counterpart = arrived.get(author);\n if (more.length > 0 || counterpart?.length !== 1) continue;\n if (declared(before[beforeIndex!]!) && declared(after[counterpart[0]!]!)) continue;\n taken.add(beforeIndex!);\n partner[counterpart[0]!] = beforeIndex;\n }\n}\n\n/** Whether a node says who it is, by the first two of `matchKey`'s keys. */\nfunction declared(node: SemanticNode): boolean {\n return node.alias !== undefined || (node.role !== undefined && node.name !== undefined);\n}\n\n/** Positions of the nodes `open` admits, by the component that created each. */\nfunction byAuthor(\n children: readonly SemanticNode[],\n open: (position: number) => boolean,\n): Map<string, number[]> {\n const authors = new Map<string, number[]>();\n\n children.forEach((child, position) => {\n const author = child.provenance?.createdBy;\n if (author === undefined || !open(position)) return;\n const positions = authors.get(author);\n if (positions) positions.push(position);\n else authors.set(author, [position]);\n });\n\n return authors;\n}\n\nfunction index(\n children: readonly SemanticNode[],\n key: (node: SemanticNode) => string,\n): Map<string, number[]> {\n const buckets = new Map<string, number[]>();\n\n children.forEach((child, position) => {\n const bucket = buckets.get(key(child));\n if (bucket) bucket.push(position);\n else buckets.set(key(child), [position]);\n });\n\n return buckets;\n}\n\n/** Lowest unclaimed index in a bucket, marking it claimed. */\nfunction claim(bucket: number[] | undefined, taken: Set<number>): number | undefined {\n const free = bucket?.find((position) => !taken.has(position));\n if (free !== undefined) taken.add(free);\n return free;\n}\n\n/**\n * Indices that must have moved for `sequence` to be in this order.\n *\n * The complement of a longest increasing subsequence: everything in that\n * subsequence kept its relative order and needs no explanation, and what is left\n * is the smallest set of movements that produces the observed sequence. A\n * prepend leaves the sequence increasing and therefore reports nothing moved,\n * which is the answer `prepend/list` asks for — one added node, and not three\n * displaced siblings alongside it.\n */\nfunction outOfOrder(sequence: readonly number[]): readonly number[] {\n if (sequence.length < 2) return [];\n\n // Patience sorting. `tails[k]` is the smallest tail of an increasing run of\n // length k+1; `previous` reconstructs which elements formed the longest one.\n const tails: number[] = [];\n const tailIndex: number[] = [];\n const previous: (number | undefined)[] = Array.from(\n { length: sequence.length },\n () => undefined,\n );\n\n for (let position = 0; position < sequence.length; position += 1) {\n const value = sequence[position]!;\n\n let low = 0;\n let high = tails.length;\n while (low < high) {\n const mid = (low + high) >> 1;\n if (tails[mid]! < value) low = mid + 1;\n else high = mid;\n }\n\n tails[low] = value;\n tailIndex[low] = position;\n previous[position] = low > 0 ? tailIndex[low - 1] : undefined;\n }\n\n const kept = new Set<number>();\n let cursor: number | undefined = tailIndex[tails.length - 1];\n while (cursor !== undefined) {\n kept.add(cursor);\n cursor = previous[cursor];\n }\n\n return sequence.filter((_, position) => !kept.has(position));\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { boundaries as componentBoundaries, UNATTRIBUTED } from '../attribute/boundary.js';
2
- import { BANDS, bandOf } from './band.js';
2
+ import { BANDS } from './band.js';
3
3
  import { compareTrees, matchTrees } from './diff/index.js';
4
4
  import { cascadeInputs, declaredIn } from './cascade.js';
5
5
  import { compareHoldings } from './holding-diff.js';
@@ -127,8 +127,10 @@ function boundaryNodes(root) {
127
127
  *
128
128
  * The fallback is not a nicety, it is the case the whole module exists for. A
129
129
  * component that took the other branch returns a `<span>` where it returned a
130
- * `<p>`, and no host-level matcher pairs those: the tag is the thing that
131
- * changed, and every identity key ends in it. Reporting the boundary as present
130
+ * `<p>`, and the host-level matcher pairs those only when the snapshot recorded
131
+ * that component as the author of both and it replaced exactly one element under
132
+ * one parent (`match.ts`). Otherwise the tag is the thing that changed, and every
133
+ * identity key that names neither an alias nor a role ends in it. Reporting the boundary as present
132
134
  * on one side only would throw away the props and the hook cells at exactly the
133
135
  * moment they are the answer — `Summary` did not appear, it *chose differently*,
134
136
  * and the evidence for that is sitting in a holding nobody joined.
@@ -205,7 +207,7 @@ function attribute(drafts, deltas) {
205
207
  }
206
208
  /** Apply the precedence in {@link PartingRung} and freeze the row. */
207
209
  function settle(draft, place) {
208
- const bands = BANDS.filter((band) => draft.owned.some((delta) => bandOf(delta.kind) === band));
210
+ const bands = BANDS.filter((band) => draft.owned.some((delta) => delta.band === band));
209
211
  const moved = [
210
212
  ...new Set(draft.owned.map((delta) => delta.property).filter((name) => name !== undefined)),
211
213
  ].sort();
@@ -1 +1 @@
1
- {"version":3,"file":"parting.js","sourceRoot":"","sources":["../../src/compare/parting.ts"],"names":[],"mappings":"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","sourcesContent":["import { boundaries as componentBoundaries, UNATTRIBUTED } from '../attribute/boundary.js';\nimport type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticNode, SemanticSnapshot } from '../format/snapshot.js';\nimport { BANDS, bandOf, type Band } from './band.js';\nimport { compareTrees, matchTrees, type Delta } from './diff/index.js';\nimport { cascadeInputs, declaredIn } from './cascade.js';\nimport { compareHoldings } from './holding-diff.js';\nimport { sameTree, sliceOf, type PartingSlice } from './slice.js';\n\n/**\n * Where two readings of one page parted.\n *\n * A diff says a `<div>` rendered a `<p>` on one side and a `<span>` on the\n * other. That is a symptom, and acting on it means reading the component,\n * guessing which branch ran, and guessing why. This says: `Cart`'s `useState` at\n * call 2 held different values, so `Summary` was handed a different `total`, so\n * the tag moved. Same evidence, three rungs up.\n *\n * The rung it can reach is decided by how much of the boundary was readable, and\n * the difference between the top two rungs is the whole point:\n *\n * - **collateral** — a prop or a context this boundary *received* moved, so the\n * difference arrived from outside and something above it is the cause.\n * - **origin** — every input this boundary received held, and its own retained\n * state moved. Nothing above it explains this; it is the answer.\n *\n * The mechanism is one rule applied at every boundary: *a component whose\n * inputs agreed and whose output moved decided differently.* Walking the tree\n * for the shallowest boundary where that holds is what turns a page of deltas\n * into one sentence — and the same rule, run over two readings of one page\n * rather than two arms of an experiment, is what separates a flake from a\n * change. A flake is the case where nothing readable moved at all\n * ({@link PartingRung} `undetermined`), and it is only nameable because\n * `unread` is a rung of its own: an accusation of nondeterminism may only be\n * made about inputs somebody actually read (ADR-0002). It is also only nameable\n * when the two readings were taken at one address — see {@link PartingPlace}.\n *\n * Not `attribute/composition.ts`'s `Divergence`, which is the same suspicion\n * arrived at from the other side: one component rendering two ways from one\n * props digest *within* a run, found by counting sites. That one can say a\n * component's inputs do not determine its output; this one can say which input.\n *\n * Nothing here reaches a hash. {@link Holding} is evidence carried beside the\n * snapshot, exactly as `styleProvenance` is, and for the reason `wiring.ts`\n * gives: a hook's value is the thing that legitimately differs between two\n * readings of one page, so a band carrying it would be a flake generator wearing\n * a band's name. This module is the reader that value was kept for.\n */\nexport interface Parting {\n /**\n * What kind of parting this is, decided before which input moved.\n *\n * The triage line. `refactor` and `settled` are pages nobody needs to open,\n * `flake` is a page whose baseline is the problem, and `variation` is the\n * only one where the rungs below are worth reading.\n */\n readonly slice: PartingSlice;\n\n /** `true` when both render hashes agree. Inputs may still have moved. */\n readonly identical: boolean;\n\n /** Every delta, unchanged, so a caller need not run the comparison twice. */\n readonly deltas: readonly Delta[];\n\n /**\n * Boundaries where something moved — an input, an output, or both — in\n * document order.\n *\n * Absent, never `[]`, when no node on either side started a component: a page\n * read with no framework adapter has not been found to agree, it has not been\n * asked (ADR-0002). Present with every boundary at rung `unread` is the next\n * reading up — the components were found and their inputs were not.\n */\n readonly boundaries?: readonly PartedBoundary[];\n\n /**\n * The boundaries the difference started at, shallowest first.\n *\n * A subset of {@link boundaries}: those whose output moved without an\n * incoming input to explain it. Usually one. More than one means the two\n * renders differ in more than one place, which is a real answer rather than a\n * failure to narrow.\n */\n readonly origins?: readonly PartedBoundary[];\n}\n\n/**\n * One component boundary, and what moved at it.\n *\n * A boundary is a component's root host node — `componentFiberOf`'s definition,\n * shared with the wiring band so the two can never disagree about where a\n * component starts. A component that vanished entirely has no boundary here: its\n * nodes leave `node-removed` deltas that attribute to the *parent* boundary,\n * which is the correct answer, because deciding not to render a child is\n * something the parent did.\n */\nexport interface PartedBoundary {\n /** `displayName`, or `(anonymous)` when provenance did not name it. */\n readonly component: string;\n\n /** Path in the candidate tree. */\n readonly path: NodePath;\n\n /** Depth of {@link path}, so callers can order without re-parsing it. */\n readonly depth: number;\n\n readonly rung: PartingRung;\n\n /**\n * Inputs that differ: props, then contexts, then hook calls in order, then\n * the inherited properties no declaration at this boundary accounts for.\n */\n readonly inputs: readonly MovedInput[];\n\n /**\n * Deltas at or under this boundary that no nested boundary owns.\n *\n * Zero is meaningful and is not a filtered-out row: a boundary with moved\n * inputs and no deltas of its own is the negative result an experiment\n * usually wants — this arm was assigned differently and rendered the same.\n */\n readonly deltas: number;\n\n /** Bands those deltas fall in, in band order. */\n readonly bands: readonly Band[];\n\n /**\n * The properties the owned deltas named, deduplicated and in code-unit order.\n *\n * The last joint of the chain the rungs above climb: a hook cell moved, a prop\n * carried it down, and *this* is what the prop turned into on the page —\n * `color`, `padding-top`, `width`. A reader chasing a visual regression is\n * looking for this list, and `deltas: 7` alone sends them back to the delta\n * array to assemble it.\n *\n * Absent when no owned delta named a property, which is a reading rather than\n * a gap: a removed node and a changed accessible name are whole-node facts and\n * have no property to name. `deltas` and `bands` still say what happened\n * there.\n */\n readonly moved?: readonly string[];\n}\n\n/**\n * How far up the difference could be traced at one boundary.\n *\n * Ordered by precedence, and the ordering is the reasoning. Any input that\n * arrived from outside settles the question — this boundary is downstream of\n * the cause, whatever else it may also be holding — so `handed` and `provided`\n * outrank the state rungs. `unread` outranks `undetermined` for the same reason\n * pointed the other way: silence is not agreement.\n */\nexport type PartingRung =\n /** A named prop differs. The parent decided this; look up. */\n | 'handed'\n /** A context value differs. A provider above decided this; look up. */\n | 'provided'\n /**\n * An inherited style value differs, and this boundary declares none of it.\n * An ancestor's cascade decided this; look up.\n */\n | 'inherited'\n /** A `useSyncExternalStore` snapshot differs — the store moved, outside React. */\n | 'external'\n /** An own hook cell differs. **This is the cause.** */\n | 'stateful'\n /** The output moved and something this boundary depends on could not be read. */\n | 'unread'\n /** Every input was read, every input agreed, and the output moved anyway. */\n | 'undetermined'\n /**\n * The same, between two readings taken in different places. Where a component\n * sits is decided by the boxes around it and is handed to it as nothing, so\n * two instances that agreed on every input and landed apart have contradicted\n * nothing. {@link PartingPlace} is how a caller says which it is asking.\n */\n | 'placed'\n /** The boundary exists on one side only. */\n | 'unpaired';\n\n/**\n * Whether two readings were taken at one address.\n *\n * `same` is a subject read twice — across two revisions, or across two moments\n * of one scenario. There, an output that moved with every input holding is the\n * accusation this system is careful about: the reading is not repeatable.\n *\n * `elsewhere` is two instances lifted out of two subjects at one commit, which\n * is `attribute/divergence.ts`' whole shape. The same evidence means something\n * different there: position, and everything a measured box decides downstream of\n * it, is a function of the context an instance was mounted in, not of the props\n * it received. Calling that nondeterminism blames a component for its page.\n */\nexport type PartingPlace = 'same' | 'elsewhere';\n\nexport interface MovedInput {\n readonly kind: 'prop' | 'context' | 'hook' | 'inherited';\n\n /**\n * Prop name, context display name, hook name (`useState`), or — for\n * `inherited` — the CSS property an ancestor decided.\n */\n readonly name: string;\n\n /** Hook call position — the index a reader gets counting down the component. */\n readonly index?: number;\n\n /**\n * Explicitly `| undefined`, on {@link Delta}'s rule: an input present on one\n * side only is the reading, not a field somebody forgot.\n */\n readonly from?: Digest | undefined;\n readonly to?: Digest | undefined;\n}\n\n/**\n * Two snapshots of the same page, read for where they parted.\n *\n * Takes snapshots rather than trees, and takes them without refusing a subject\n * mismatch, for `compareTrees`' stated reason: this produces an *explanation*,\n * not a verdict, and two arms of an experiment are two subjects on purpose.\n */\nexport function partingOf(\n baseline: SemanticSnapshot,\n candidate: SemanticSnapshot,\n place: PartingPlace = 'same',\n): Parting {\n const comparison = compareTrees(baseline, candidate);\n const matching = matchTrees(baseline.root, candidate.root);\n\n const partner = new Map<SemanticNode, SemanticNode>();\n for (const [left, right] of matching.pairs) partner.set(right, left);\n\n const tree = sameTree(baseline.root, candidate.root);\n const moved = comparison.deltas.length > 0;\n\n const found = boundariesOf(baseline.root, candidate.root, partner, {\n there: declaredIn(baseline),\n here: declaredIn(candidate),\n });\n if (found === undefined) {\n return {\n slice: sliceOf(tree, undefined, moved, place),\n identical: comparison.identical,\n deltas: comparison.deltas,\n };\n }\n\n attribute(found, comparison.deltas);\n\n const boundaries = found\n .filter((entry) => entry.inputs.length > 0 || entry.owned.length > 0)\n .map((draft) => settle(draft, place));\n\n const origins = boundaries.filter(\n (entry) =>\n entry.rung === 'stateful' ||\n entry.rung === 'external' ||\n entry.rung === 'undetermined' ||\n entry.rung === 'placed',\n );\n\n return {\n slice: sliceOf(tree, boundaries, moved, place),\n identical: comparison.identical,\n deltas: comparison.deltas,\n boundaries,\n ...(origins.length > 0 ? { origins: [...origins].sort((a, b) => a.depth - b.depth) } : {}),\n };\n}\n\n/** A boundary mid-construction, before its deltas are counted. */\ninterface Draft {\n readonly component: string;\n readonly path: NodePath;\n readonly basePath?: NodePath;\n readonly depth: number;\n readonly inputs: readonly MovedInput[];\n readonly unread: boolean;\n readonly paired: boolean;\n readonly owned: Delta[];\n}\n\n/**\n * Every boundary in the candidate tree, in document order, paired with its\n * baseline counterpart.\n *\n * Undefined when neither tree carried a holding at all, which is the ADR-0002\n * distinction this whole module rests on: nothing to read is not nothing to\n * report.\n */\nfunction boundariesOf(\n baselineRoot: SemanticNode,\n candidateRoot: SemanticNode,\n partner: ReadonlyMap<SemanticNode, SemanticNode>,\n declared: { readonly there: ReadonlySet<string>; readonly here: ReadonlySet<string> },\n): Draft[] | undefined {\n const mine = boundaryNodes(candidateRoot);\n const theirs = boundaryNodes(baselineRoot);\n if (mine.length === 0 && theirs.length === 0) return undefined;\n\n const paired = pairBoundaries(mine, theirs, partner);\n\n return mine.map((node) => {\n const other = paired.get(node);\n const moved = compareHoldings(other?.holding, node.holding);\n const inputs = [\n ...moved.inputs,\n ...cascadeInputs(other, node, declared.there, declared.here),\n ];\n return {\n component: node.provenance?.owners[0]?.name ?? '(anonymous)',\n path: node.path,\n ...(other === undefined ? {} : { basePath: other.path }),\n depth: depthOf(node.path),\n inputs,\n unread: moved.unread,\n paired: other !== undefined,\n owned: [],\n };\n });\n}\n\n/**\n * Every node where a component starts, in document order.\n *\n * Read from provenance and not from `holding`, which is the difference between\n * this module working on one collector and working on all of them. A holding is\n * what a *framework adapter* managed to read at a boundary — props values, hook\n * cells — and only `@variance-authority/unit-test` supplies one today. Keying the\n * boundary set on it made every rung unreachable from a browser run, including\n * the two that need no adapter at all: the ancestor cascade, which is read from\n * `styleProvenance`, and the tree comparison, which is read from the owner\n * chains. Those runs got `unread` — \"nothing can be said about why\" — while\n * holding the evidence to say it.\n *\n * {@link componentBoundaries} is the shared definition, so a boundary here is\n * the same node the component hashes and the wiring band call one. It reports a\n * node once per component that opens there; the nodes are what this needs, and\n * `boundariesOf` names each by its innermost owner exactly as before.\n *\n * A node with a holding and no provenance is still kept. That is an adapter that\n * read the fiber's values but not its owner chain, and dropping its evidence\n * because of a second failure would lose the one rung it can still reach.\n */\nfunction boundaryNodes(root: SemanticNode): readonly SemanticNode[] {\n const found = new Set<SemanticNode>();\n for (const boundary of componentBoundaries(root)) {\n // `(unattributed)` is the bucket for nodes whose owner chain broke, not a\n // component, and `causesBetween` refuses it for the same reason: a boundary\n // reported there would name a component nobody wrote and pool unrelated\n // parts of the page under one name.\n if (boundary.component === UNATTRIBUTED) continue;\n found.add(boundary.node);\n }\n\n const holders = (node: SemanticNode): void => {\n if (node.holding !== undefined) found.add(node);\n for (const child of node.children) holders(child);\n };\n holders(root);\n\n return [...found].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n\n/**\n * Pair boundaries, preferring the tree matcher and falling back to the owner\n * chain.\n *\n * The fallback is not a nicety, it is the case the whole module exists for. A\n * component that took the other branch returns a `<span>` where it returned a\n * `<p>`, and no host-level matcher pairs those: the tag is the thing that\n * changed, and every identity key ends in it. Reporting the boundary as present\n * on one side only would throw away the props and the hook cells at exactly the\n * moment they are the answer — `Summary` did not appear, it *chose differently*,\n * and the evidence for that is sitting in a holding nobody joined.\n *\n * The chain — the boundary's owner names, outermost component included — is\n * stable under precisely that change, since neither the branch nor the tag\n * appears in it. Ties inside one chain are broken by document order, which is\n * positional and therefore shifts when a sibling is inserted; that is the same\n * cost `match.ts` pays for the same reason, and it is bounded here to\n * components sharing one chain that the matcher already failed to pair.\n */\nfunction pairBoundaries(\n mine: readonly SemanticNode[],\n theirs: readonly SemanticNode[],\n partner: ReadonlyMap<SemanticNode, SemanticNode>,\n): ReadonlyMap<SemanticNode, SemanticNode> {\n const paired = new Map<SemanticNode, SemanticNode>();\n const taken = new Set<SemanticNode>();\n\n for (const node of mine) {\n const other = partner.get(node);\n if (other?.holding === undefined || taken.has(other)) continue;\n paired.set(node, other);\n taken.add(other);\n }\n\n const spare = new Map<string, SemanticNode[]>();\n for (const node of theirs) {\n if (taken.has(node)) continue;\n const key = chainOf(node);\n const bucket = spare.get(key);\n if (bucket === undefined) spare.set(key, [node]);\n else bucket.push(node);\n }\n\n for (const node of mine) {\n if (paired.has(node)) continue;\n const bucket = spare.get(chainOf(node));\n const other = bucket?.shift();\n if (other !== undefined) paired.set(node, other);\n }\n\n return paired;\n}\n\nfunction chainOf(node: SemanticNode): string {\n return (node.provenance?.owners ?? []).map((owner) => owner.name).join('>');\n}\n\n/**\n * Give each delta to the deepest boundary that encloses it.\n *\n * By prefix rather than by scanning every boundary: a node's path *is* its\n * ancestry, so walking it up one segment at a time reaches the owning boundary\n * in as many steps as the node is deep. A `node-removed` delta carries a\n * baseline path, so it is resolved in baseline space — mixing the two spaces\n * would silently hand a removal to whichever candidate node now sits at that\n * address, which is the exact misattribution positional diffing is famous for.\n */\nfunction attribute(drafts: readonly Draft[], deltas: readonly Delta[]): void {\n const here = new Map<NodePath, Draft>();\n const there = new Map<NodePath, Draft>();\n for (const draft of drafts) {\n here.set(draft.path, draft);\n if (draft.basePath !== undefined) there.set(draft.basePath, draft);\n }\n\n for (const delta of deltas) {\n const index = delta.kind === 'node-removed' ? there : here;\n for (let path: NodePath | undefined = delta.path; path !== undefined; path = parentOf(path)) {\n const owner = index.get(path);\n if (owner !== undefined) {\n owner.owned.push(delta);\n break;\n }\n }\n }\n}\n\n/** Apply the precedence in {@link PartingRung} and freeze the row. */\nfunction settle(draft: Draft, place: PartingPlace): PartedBoundary {\n const bands = BANDS.filter((band) => draft.owned.some((delta) => bandOf(delta.kind) === band));\n\n const moved = [\n ...new Set(draft.owned.map((delta) => delta.property).filter((name) => name !== undefined)),\n ].sort();\n\n return {\n component: draft.component,\n path: draft.path,\n depth: draft.depth,\n rung: rungOf(draft, place),\n inputs: draft.inputs,\n deltas: draft.owned.length,\n bands,\n ...(moved.length > 0 ? { moved } : {}),\n };\n}\n\nfunction rungOf(draft: Draft, place: PartingPlace): PartingRung {\n if (!draft.paired) return 'unpaired';\n if (draft.inputs.some((input) => input.kind === 'prop')) return 'handed';\n if (draft.inputs.some((input) => input.kind === 'context')) return 'provided';\n if (draft.inputs.some((input) => input.kind === 'inherited')) return 'inherited';\n if (draft.inputs.some((input) => input.name === 'useSyncExternalStore')) return 'external';\n if (draft.inputs.length > 0) return 'stateful';\n if (draft.unread) return 'unread';\n return place === 'same' ? 'undetermined' : 'placed';\n}\n\n\nfunction depthOf(path: NodePath): number {\n return path === '' ? 0 : path.split('/').length;\n}\n\nfunction parentOf(path: NodePath): NodePath | undefined {\n if (path === '') return undefined;\n const cut = path.lastIndexOf('/');\n return cut === -1 ? '' : path.slice(0, cut);\n}\n"]}
1
+ {"version":3,"file":"parting.js","sourceRoot":"","sources":["../../src/compare/parting.ts"],"names":[],"mappings":"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","sourcesContent":["import { boundaries as componentBoundaries, UNATTRIBUTED } from '../attribute/boundary.js';\nimport type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticNode, SemanticSnapshot } from '../format/snapshot.js';\nimport { BANDS, type Band } from './band.js';\nimport { compareTrees, matchTrees, type Delta } from './diff/index.js';\nimport { cascadeInputs, declaredIn } from './cascade.js';\nimport { compareHoldings } from './holding-diff.js';\nimport { sameTree, sliceOf, type PartingSlice } from './slice.js';\n\n/**\n * Where two readings of one page parted.\n *\n * A diff says a `<div>` rendered a `<p>` on one side and a `<span>` on the\n * other. That is a symptom, and acting on it means reading the component,\n * guessing which branch ran, and guessing why. This says: `Cart`'s `useState` at\n * call 2 held different values, so `Summary` was handed a different `total`, so\n * the tag moved. Same evidence, three rungs up.\n *\n * The rung it can reach is decided by how much of the boundary was readable, and\n * the difference between the top two rungs is the whole point:\n *\n * - **collateral** — a prop or a context this boundary *received* moved, so the\n * difference arrived from outside and something above it is the cause.\n * - **origin** — every input this boundary received held, and its own retained\n * state moved. Nothing above it explains this; it is the answer.\n *\n * The mechanism is one rule applied at every boundary: *a component whose\n * inputs agreed and whose output moved decided differently.* Walking the tree\n * for the shallowest boundary where that holds is what turns a page of deltas\n * into one sentence — and the same rule, run over two readings of one page\n * rather than two arms of an experiment, is what separates a flake from a\n * change. A flake is the case where nothing readable moved at all\n * ({@link PartingRung} `undetermined`), and it is only nameable because\n * `unread` is a rung of its own: an accusation of nondeterminism may only be\n * made about inputs somebody actually read (ADR-0002). It is also only nameable\n * when the two readings were taken at one address — see {@link PartingPlace}.\n *\n * Not `attribute/composition.ts`'s `Divergence`, which is the same suspicion\n * arrived at from the other side: one component rendering two ways from one\n * props digest *within* a run, found by counting sites. That one can say a\n * component's inputs do not determine its output; this one can say which input.\n *\n * Nothing here reaches a hash. {@link Holding} is evidence carried beside the\n * snapshot, exactly as `styleProvenance` is, and for the reason `wiring.ts`\n * gives: a hook's value is the thing that legitimately differs between two\n * readings of one page, so a band carrying it would be a flake generator wearing\n * a band's name. This module is the reader that value was kept for.\n */\nexport interface Parting {\n /**\n * What kind of parting this is, decided before which input moved.\n *\n * The triage line. `refactor` and `settled` are pages nobody needs to open,\n * `flake` is a page whose baseline is the problem, and `variation` is the\n * only one where the rungs below are worth reading.\n */\n readonly slice: PartingSlice;\n\n /** `true` when both render hashes agree. Inputs may still have moved. */\n readonly identical: boolean;\n\n /** Every delta, unchanged, so a caller need not run the comparison twice. */\n readonly deltas: readonly Delta[];\n\n /**\n * Boundaries where something moved — an input, an output, or both — in\n * document order.\n *\n * Absent, never `[]`, when no node on either side started a component: a page\n * read with no framework adapter has not been found to agree, it has not been\n * asked (ADR-0002). Present with every boundary at rung `unread` is the next\n * reading up — the components were found and their inputs were not.\n */\n readonly boundaries?: readonly PartedBoundary[];\n\n /**\n * The boundaries the difference started at, shallowest first.\n *\n * A subset of {@link boundaries}: those whose output moved without an\n * incoming input to explain it. Usually one. More than one means the two\n * renders differ in more than one place, which is a real answer rather than a\n * failure to narrow.\n */\n readonly origins?: readonly PartedBoundary[];\n}\n\n/**\n * One component boundary, and what moved at it.\n *\n * A boundary is a component's root host node — `componentFiberOf`'s definition,\n * shared with the wiring band so the two can never disagree about where a\n * component starts. A component that vanished entirely has no boundary here: its\n * nodes leave `node-removed` deltas that attribute to the *parent* boundary,\n * which is the correct answer, because deciding not to render a child is\n * something the parent did.\n */\nexport interface PartedBoundary {\n /** `displayName`, or `(anonymous)` when provenance did not name it. */\n readonly component: string;\n\n /** Path in the candidate tree. */\n readonly path: NodePath;\n\n /** Depth of {@link path}, so callers can order without re-parsing it. */\n readonly depth: number;\n\n readonly rung: PartingRung;\n\n /**\n * Inputs that differ: props, then contexts, then hook calls in order, then\n * the inherited properties no declaration at this boundary accounts for.\n */\n readonly inputs: readonly MovedInput[];\n\n /**\n * Deltas at or under this boundary that no nested boundary owns.\n *\n * Zero is meaningful and is not a filtered-out row: a boundary with moved\n * inputs and no deltas of its own is the negative result an experiment\n * usually wants — this arm was assigned differently and rendered the same.\n */\n readonly deltas: number;\n\n /** Bands those deltas fall in, in band order. */\n readonly bands: readonly Band[];\n\n /**\n * The properties the owned deltas named, deduplicated and in code-unit order.\n *\n * The last joint of the chain the rungs above climb: a hook cell moved, a prop\n * carried it down, and *this* is what the prop turned into on the page —\n * `color`, `padding-top`, `width`. A reader chasing a visual regression is\n * looking for this list, and `deltas: 7` alone sends them back to the delta\n * array to assemble it.\n *\n * Absent when no owned delta named a property, which is a reading rather than\n * a gap: a removed node and a changed accessible name are whole-node facts and\n * have no property to name. `deltas` and `bands` still say what happened\n * there.\n */\n readonly moved?: readonly string[];\n}\n\n/**\n * How far up the difference could be traced at one boundary.\n *\n * Ordered by precedence, and the ordering is the reasoning. Any input that\n * arrived from outside settles the question — this boundary is downstream of\n * the cause, whatever else it may also be holding — so `handed` and `provided`\n * outrank the state rungs. `unread` outranks `undetermined` for the same reason\n * pointed the other way: silence is not agreement.\n */\nexport type PartingRung =\n /** A named prop differs. The parent decided this; look up. */\n | 'handed'\n /** A context value differs. A provider above decided this; look up. */\n | 'provided'\n /**\n * An inherited style value differs, and this boundary declares none of it.\n * An ancestor's cascade decided this; look up.\n */\n | 'inherited'\n /** A `useSyncExternalStore` snapshot differs — the store moved, outside React. */\n | 'external'\n /** An own hook cell differs. **This is the cause.** */\n | 'stateful'\n /** The output moved and something this boundary depends on could not be read. */\n | 'unread'\n /** Every input was read, every input agreed, and the output moved anyway. */\n | 'undetermined'\n /**\n * The same, between two readings taken in different places. Where a component\n * sits is decided by the boxes around it and is handed to it as nothing, so\n * two instances that agreed on every input and landed apart have contradicted\n * nothing. {@link PartingPlace} is how a caller says which it is asking.\n */\n | 'placed'\n /** The boundary exists on one side only. */\n | 'unpaired';\n\n/**\n * Whether two readings were taken at one address.\n *\n * `same` is a subject read twice — across two revisions, or across two moments\n * of one scenario. There, an output that moved with every input holding is the\n * accusation this system is careful about: the reading is not repeatable.\n *\n * `elsewhere` is two instances lifted out of two subjects at one commit, which\n * is `attribute/divergence.ts`' whole shape. The same evidence means something\n * different there: position, and everything a measured box decides downstream of\n * it, is a function of the context an instance was mounted in, not of the props\n * it received. Calling that nondeterminism blames a component for its page.\n */\nexport type PartingPlace = 'same' | 'elsewhere';\n\nexport interface MovedInput {\n readonly kind: 'prop' | 'context' | 'hook' | 'inherited';\n\n /**\n * Prop name, context display name, hook name (`useState`), or — for\n * `inherited` — the CSS property an ancestor decided.\n */\n readonly name: string;\n\n /** Hook call position — the index a reader gets counting down the component. */\n readonly index?: number;\n\n /**\n * Explicitly `| undefined`, on {@link Delta}'s rule: an input present on one\n * side only is the reading, not a field somebody forgot.\n */\n readonly from?: Digest | undefined;\n readonly to?: Digest | undefined;\n}\n\n/**\n * Two snapshots of the same page, read for where they parted.\n *\n * Takes snapshots rather than trees, and takes them without refusing a subject\n * mismatch, for `compareTrees`' stated reason: this produces an *explanation*,\n * not a verdict, and two arms of an experiment are two subjects on purpose.\n */\nexport function partingOf(\n baseline: SemanticSnapshot,\n candidate: SemanticSnapshot,\n place: PartingPlace = 'same',\n): Parting {\n const comparison = compareTrees(baseline, candidate);\n const matching = matchTrees(baseline.root, candidate.root);\n\n const partner = new Map<SemanticNode, SemanticNode>();\n for (const [left, right] of matching.pairs) partner.set(right, left);\n\n const tree = sameTree(baseline.root, candidate.root);\n const moved = comparison.deltas.length > 0;\n\n const found = boundariesOf(baseline.root, candidate.root, partner, {\n there: declaredIn(baseline),\n here: declaredIn(candidate),\n });\n if (found === undefined) {\n return {\n slice: sliceOf(tree, undefined, moved, place),\n identical: comparison.identical,\n deltas: comparison.deltas,\n };\n }\n\n attribute(found, comparison.deltas);\n\n const boundaries = found\n .filter((entry) => entry.inputs.length > 0 || entry.owned.length > 0)\n .map((draft) => settle(draft, place));\n\n const origins = boundaries.filter(\n (entry) =>\n entry.rung === 'stateful' ||\n entry.rung === 'external' ||\n entry.rung === 'undetermined' ||\n entry.rung === 'placed',\n );\n\n return {\n slice: sliceOf(tree, boundaries, moved, place),\n identical: comparison.identical,\n deltas: comparison.deltas,\n boundaries,\n ...(origins.length > 0 ? { origins: [...origins].sort((a, b) => a.depth - b.depth) } : {}),\n };\n}\n\n/** A boundary mid-construction, before its deltas are counted. */\ninterface Draft {\n readonly component: string;\n readonly path: NodePath;\n readonly basePath?: NodePath;\n readonly depth: number;\n readonly inputs: readonly MovedInput[];\n readonly unread: boolean;\n readonly paired: boolean;\n readonly owned: Delta[];\n}\n\n/**\n * Every boundary in the candidate tree, in document order, paired with its\n * baseline counterpart.\n *\n * Undefined when neither tree carried a holding at all, which is the ADR-0002\n * distinction this whole module rests on: nothing to read is not nothing to\n * report.\n */\nfunction boundariesOf(\n baselineRoot: SemanticNode,\n candidateRoot: SemanticNode,\n partner: ReadonlyMap<SemanticNode, SemanticNode>,\n declared: { readonly there: ReadonlySet<string>; readonly here: ReadonlySet<string> },\n): Draft[] | undefined {\n const mine = boundaryNodes(candidateRoot);\n const theirs = boundaryNodes(baselineRoot);\n if (mine.length === 0 && theirs.length === 0) return undefined;\n\n const paired = pairBoundaries(mine, theirs, partner);\n\n return mine.map((node) => {\n const other = paired.get(node);\n const moved = compareHoldings(other?.holding, node.holding);\n const inputs = [\n ...moved.inputs,\n ...cascadeInputs(other, node, declared.there, declared.here),\n ];\n return {\n component: node.provenance?.owners[0]?.name ?? '(anonymous)',\n path: node.path,\n ...(other === undefined ? {} : { basePath: other.path }),\n depth: depthOf(node.path),\n inputs,\n unread: moved.unread,\n paired: other !== undefined,\n owned: [],\n };\n });\n}\n\n/**\n * Every node where a component starts, in document order.\n *\n * Read from provenance and not from `holding`, which is the difference between\n * this module working on one collector and working on all of them. A holding is\n * what a *framework adapter* managed to read at a boundary — props values, hook\n * cells — and only `@variance-authority/unit-test` supplies one today. Keying the\n * boundary set on it made every rung unreachable from a browser run, including\n * the two that need no adapter at all: the ancestor cascade, which is read from\n * `styleProvenance`, and the tree comparison, which is read from the owner\n * chains. Those runs got `unread` — \"nothing can be said about why\" — while\n * holding the evidence to say it.\n *\n * {@link componentBoundaries} is the shared definition, so a boundary here is\n * the same node the component hashes and the wiring band call one. It reports a\n * node once per component that opens there; the nodes are what this needs, and\n * `boundariesOf` names each by its innermost owner exactly as before.\n *\n * A node with a holding and no provenance is still kept. That is an adapter that\n * read the fiber's values but not its owner chain, and dropping its evidence\n * because of a second failure would lose the one rung it can still reach.\n */\nfunction boundaryNodes(root: SemanticNode): readonly SemanticNode[] {\n const found = new Set<SemanticNode>();\n for (const boundary of componentBoundaries(root)) {\n // `(unattributed)` is the bucket for nodes whose owner chain broke, not a\n // component, and `causesBetween` refuses it for the same reason: a boundary\n // reported there would name a component nobody wrote and pool unrelated\n // parts of the page under one name.\n if (boundary.component === UNATTRIBUTED) continue;\n found.add(boundary.node);\n }\n\n const holders = (node: SemanticNode): void => {\n if (node.holding !== undefined) found.add(node);\n for (const child of node.children) holders(child);\n };\n holders(root);\n\n return [...found].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n\n/**\n * Pair boundaries, preferring the tree matcher and falling back to the owner\n * chain.\n *\n * The fallback is not a nicety, it is the case the whole module exists for. A\n * component that took the other branch returns a `<span>` where it returned a\n * `<p>`, and the host-level matcher pairs those only when the snapshot recorded\n * that component as the author of both and it replaced exactly one element under\n * one parent (`match.ts`). Otherwise the tag is the thing that changed, and every\n * identity key that names neither an alias nor a role ends in it. Reporting the boundary as present\n * on one side only would throw away the props and the hook cells at exactly the\n * moment they are the answer — `Summary` did not appear, it *chose differently*,\n * and the evidence for that is sitting in a holding nobody joined.\n *\n * The chain — the boundary's owner names, outermost component included — is\n * stable under precisely that change, since neither the branch nor the tag\n * appears in it. Ties inside one chain are broken by document order, which is\n * positional and therefore shifts when a sibling is inserted; that is the same\n * cost `match.ts` pays for the same reason, and it is bounded here to\n * components sharing one chain that the matcher already failed to pair.\n */\nfunction pairBoundaries(\n mine: readonly SemanticNode[],\n theirs: readonly SemanticNode[],\n partner: ReadonlyMap<SemanticNode, SemanticNode>,\n): ReadonlyMap<SemanticNode, SemanticNode> {\n const paired = new Map<SemanticNode, SemanticNode>();\n const taken = new Set<SemanticNode>();\n\n for (const node of mine) {\n const other = partner.get(node);\n if (other?.holding === undefined || taken.has(other)) continue;\n paired.set(node, other);\n taken.add(other);\n }\n\n const spare = new Map<string, SemanticNode[]>();\n for (const node of theirs) {\n if (taken.has(node)) continue;\n const key = chainOf(node);\n const bucket = spare.get(key);\n if (bucket === undefined) spare.set(key, [node]);\n else bucket.push(node);\n }\n\n for (const node of mine) {\n if (paired.has(node)) continue;\n const bucket = spare.get(chainOf(node));\n const other = bucket?.shift();\n if (other !== undefined) paired.set(node, other);\n }\n\n return paired;\n}\n\nfunction chainOf(node: SemanticNode): string {\n return (node.provenance?.owners ?? []).map((owner) => owner.name).join('>');\n}\n\n/**\n * Give each delta to the deepest boundary that encloses it.\n *\n * By prefix rather than by scanning every boundary: a node's path *is* its\n * ancestry, so walking it up one segment at a time reaches the owning boundary\n * in as many steps as the node is deep. A `node-removed` delta carries a\n * baseline path, so it is resolved in baseline space — mixing the two spaces\n * would silently hand a removal to whichever candidate node now sits at that\n * address, which is the exact misattribution positional diffing is famous for.\n */\nfunction attribute(drafts: readonly Draft[], deltas: readonly Delta[]): void {\n const here = new Map<NodePath, Draft>();\n const there = new Map<NodePath, Draft>();\n for (const draft of drafts) {\n here.set(draft.path, draft);\n if (draft.basePath !== undefined) there.set(draft.basePath, draft);\n }\n\n for (const delta of deltas) {\n const index = delta.kind === 'node-removed' ? there : here;\n for (let path: NodePath | undefined = delta.path; path !== undefined; path = parentOf(path)) {\n const owner = index.get(path);\n if (owner !== undefined) {\n owner.owned.push(delta);\n break;\n }\n }\n }\n}\n\n/** Apply the precedence in {@link PartingRung} and freeze the row. */\nfunction settle(draft: Draft, place: PartingPlace): PartedBoundary {\n const bands = BANDS.filter((band) => draft.owned.some((delta) => delta.band === band));\n\n const moved = [\n ...new Set(draft.owned.map((delta) => delta.property).filter((name) => name !== undefined)),\n ].sort();\n\n return {\n component: draft.component,\n path: draft.path,\n depth: draft.depth,\n rung: rungOf(draft, place),\n inputs: draft.inputs,\n deltas: draft.owned.length,\n bands,\n ...(moved.length > 0 ? { moved } : {}),\n };\n}\n\nfunction rungOf(draft: Draft, place: PartingPlace): PartingRung {\n if (!draft.paired) return 'unpaired';\n if (draft.inputs.some((input) => input.kind === 'prop')) return 'handed';\n if (draft.inputs.some((input) => input.kind === 'context')) return 'provided';\n if (draft.inputs.some((input) => input.kind === 'inherited')) return 'inherited';\n if (draft.inputs.some((input) => input.name === 'useSyncExternalStore')) return 'external';\n if (draft.inputs.length > 0) return 'stateful';\n if (draft.unread) return 'unread';\n return place === 'same' ? 'undetermined' : 'placed';\n}\n\n\nfunction depthOf(path: NodePath): number {\n return path === '' ? 0 : path.split('/').length;\n}\n\nfunction parentOf(path: NodePath): NodePath | undefined {\n if (path === '') return undefined;\n const cut = path.lastIndexOf('/');\n return cut === -1 ? '' : path.slice(0, cut);\n}\n"]}
@@ -61,6 +61,15 @@ export interface RawCapture {
61
61
  * Recorded so the coupling can be reported before it bites.
62
62
  */
63
63
  readonly couplings?: readonly string[];
64
+ /**
65
+ * The document's base URL, `document.baseURI`, as the page reported it.
66
+ *
67
+ * An engine resolves every `url()` in a computed value against this, so a
68
+ * build served on a different port comes back with different strings. The
69
+ * normalizer drops this origin from them. Absent when the collector had no
70
+ * page to ask, and then nothing is dropped.
71
+ */
72
+ readonly baseUrl?: string;
64
73
  /** Anything the collector could not do. Empty is the expected case. */
65
74
  readonly diagnostics: readonly Diagnostic[];
66
75
  }
@@ -88,6 +97,11 @@ export interface SubjectRef {
88
97
  * ruleset, versioned by nothing.
89
98
  */
90
99
  export interface RawNode {
100
+ /**
101
+ * Lower-case element name, `#text` for a text run, or `::before` / `::after`
102
+ * for a box CSS generates inside its parent — children with no DOM node,
103
+ * which paint like any other child.
104
+ */
91
105
  readonly tag: string;
92
106
  readonly attributes: Readonly<Record<string, string>>;
93
107
  /** Resolved accessibility properties, when the profile provides them. */
@@ -154,6 +168,15 @@ export interface RawNode {
154
168
  * re-baseline the repository.
155
169
  */
156
170
  readonly ignoredBy?: readonly string[];
171
+ /**
172
+ * Why part of this node was not read, on a node whose existence is known and
173
+ * whose values are not — a `::before` whose `content` holds a `counter()` under
174
+ * a host with no layout engine.
175
+ *
176
+ * A marker, never a value: the fields it describes are absent rather than
177
+ * guessed (ADR-0002). Outside every hash, like `ignoredBy` above it.
178
+ */
179
+ readonly unread?: string;
157
180
  }
158
181
  export interface RawAria {
159
182
  readonly role: string | null;
@@ -203,6 +226,8 @@ export interface Rect {
203
226
  readonly width: number;
204
227
  readonly height: number;
205
228
  }
229
+ /** The area two rects share; zero when they only touch or do not meet. */
230
+ export declare function intersectionArea(a: Rect, b: Rect): number;
206
231
  export interface Diagnostic {
207
232
  readonly severity: 'warn' | 'error';
208
233
  readonly code: string;
@@ -1,3 +1,9 @@
1
+ /** The area two rects share; zero when they only touch or do not meet. */
2
+ export function intersectionArea(a, b) {
3
+ const width = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
4
+ const height = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
5
+ return width > 0 && height > 0 ? width * height : 0;
6
+ }
1
7
  /**
2
8
  * One list from several, with a diagnostic said once however many times it was
3
9
  * found.
@@ -1 +1 @@
1
- {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/format/capture.ts"],"names":[],"mappings":"AA8OA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAG,KAAqD;IAExD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAC;IACjD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,UAAU,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;YACpC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;gBACzB,UAAU,CAAC,QAAQ;gBACnB,UAAU,CAAC,IAAI;gBACf,UAAU,CAAC,OAAO;gBAClB,UAAU,CAAC,QAAQ,IAAI,IAAI;aAC5B,CAAC,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;AAClC,CAAC","sourcesContent":["import type { EnvironmentInputs } from './environment.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The collector/core boundary.\n *\n * Collectors **extract**; `core` **normalizes**. Nothing in this file is a live\n * object — a `RawCapture` is plain data, fully serializable, produced by touching\n * a DOM and consumed by code that has never seen one.\n *\n * Two properties fall out of that split, and both are the reason for it:\n *\n * 1. **One ruleset, by construction.** Normalization is the moat (ADR-0003) and\n * the thing most likely to drift between two implementations. If the JSDOM and\n * Chromium collectors each normalized their own captures, \"the same rules ran\"\n * would be a claim maintained by discipline. Here it is a fact of the call\n * graph: there is one normalizer and both profiles enter it.\n * 2. **The sub-renderer can be anywhere.** A capture crossing a worker boundary,\n * a pipe, or a network hop to a device farm is the same value it was in\n * process. No code may assume the collector is local.\n */\nexport interface RawCapture {\n readonly captureVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n\n /**\n * Environment inputs the *collector* is positioned to know — engine version,\n * fonts actually loaded, resolved conditions, asset hashes. The ruleset and\n * allowlist versions are `core`'s to supply, so they are absent here and the\n * key is completed during normalization.\n */\n readonly environment: Omit<EnvironmentInputs, 'ruleset' | 'allowlist'>;\n\n readonly root: RawNode;\n\n /**\n * Inherited values in force at the subject root.\n *\n * Mandatory, not an optimization. CSS applicability pruning (ADR-0003) drops\n * every rule that matches nothing inside the subtree — including rules on\n * ancestors *outside* it whose inheritable properties still reach in. Without\n * this seed the cheap tier is unsound and will report false `unchanged`.\n */\n readonly inheritedSeed: Readonly<Record<string, string>>;\n\n /**\n * Subtrees this subject renders through portals, in fiber traversal order.\n *\n * A subject's boundary is a component-tree question, not a DOM-containment\n * one: `createPortal` renders elsewhere in the document while remaining part\n * of the tree rooted here. Omitting these makes an opening modal report\n * `unchanged`, because its own container is byte-identical (ADR-0007).\n */\n readonly portals?: readonly RawNode[];\n\n /**\n * Shared root state this subject is *latently* coupled to.\n *\n * Rules like `html.dark .card` contribute nothing today and everything the\n * moment `dark` lands on `<html>`. In a session that reuses one document across\n * subjects, that moment may arrive because a *different* subject put it there —\n * turning this subject's baseline order-dependent with no visible cause.\n * Recorded so the coupling can be reported before it bites.\n */\n readonly couplings?: readonly string[];\n\n /** Anything the collector could not do. Empty is the expected case. */\n readonly diagnostics: readonly Diagnostic[];\n}\n\nexport interface SubjectRef {\n /** Stable across renames of the file, e.g. `story:components-button--primary`. */\n readonly id: string;\n\n /**\n * What kind of thing this is, which is not the same question as what produced\n * it.\n *\n * `value` is the one that is not a rendering: a JSON body, a schema, a route\n * table. It is a fourth member rather than a reuse of `route`, which already\n * means *a page rendered at a URL* — a route table is a value about the same\n * paths, and a subject list where one word meant both is one nobody can filter.\n */\n readonly kind: 'story' | 'route' | 'fixture' | 'value';\n readonly title?: string;\n}\n\n/**\n * A node exactly as observed, before any rule is applied.\n *\n * Raw means raw: `attributes` still holds generated ids and hashed class names,\n * `matchedRules` still holds cascade losers. Normalization needs the unedited\n * input — a collector that helpfully pre-cleaned would be a second, invisible\n * ruleset, versioned by nothing.\n */\nexport interface RawNode {\n readonly tag: string;\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Resolved accessibility properties, when the profile provides them. */\n readonly aria?: RawAria;\n\n /**\n * Rules matching this node, as authored.\n *\n * Losing declarations are retained: `core` resolves the cascade for profiles\n * whose engine did not (ADR-0003 step 5), and cannot do that from winners\n * alone. Shorthands are retained *unexpanded* — expansion is `core`'s job, so\n * that it is versioned by the ruleset rather than by each collector.\n *\n * A collector MAY drop a declaration whose property is neither allowlisted nor\n * a shorthand of an allowlisted property, and SHOULD when the capture crosses\n * a network hop. It MUST NOT normalize values; a helpfully pre-cleaned capture\n * is a second, invisible ruleset versioned by nothing.\n */\n readonly matchedRules: readonly MatchedRule[];\n\n readonly inlineStyle?: Readonly<Record<string, string>>;\n\n /** Engine-resolved computed style. Present only when `profile.computedStyle`. */\n readonly computedStyle?: Readonly<Record<string, string>>;\n\n /** Present only when `profile.layout`. */\n readonly rect?: Rect;\n\n /** Literal text of a text node. Digested during normalization, per policy. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when an adapter supplied it.\n *\n * Beside `provenance` because it arrives from the same seam and under the same\n * rule: the collector carries no framework dependency and both are injected.\n * See {@link Wiring} for why it is a dimension rather than another attribution\n * field.\n */\n readonly wiring?: Wiring;\n\n /**\n * What the component that authored this node was holding, when an adapter\n * supplied it.\n *\n * A third injection at the same seam, and deliberately not folded into\n * `wiring`: wiring is hashed into a band and this may never be, so a caller\n * that wants the band must be able to have it without also carrying values,\n * and a caller that wants explanations must be able to ask for them without\n * changing a single digest. See {@link Holding}.\n */\n readonly holding?: Holding;\n\n readonly children: readonly RawNode[];\n\n /** Nodes inside a shadow root, kept distinct from light-DOM children. */\n readonly shadowChildren?: readonly RawNode[];\n\n /**\n * Ignore rules the collector found this element under (spec 0024).\n *\n * A mark, not a removal. The collector is the only thing holding a live DOM, so\n * it is the only thing that can run a selector or read a marker attribute — but\n * it must not act on what it finds. An element deleted from the capture would\n * be invisible to every count downstream, and the difference between \"absorbed\n * by the `carousel` rule\" and \"was never there\" is the difference between an\n * ignore and a blind spot.\n *\n * Never hashed. Normalization turns these into\n * {@link import('./snapshot.js').SemanticSnapshot.ignoreSites}, which sits\n * beside `styleProvenance` outside the render hash for the same reason: masking\n * a clock changes what a run *says*, not what it *renders*, and must not\n * re-baseline the repository.\n */\n readonly ignoredBy?: readonly string[];\n}\n\nexport interface RawAria {\n readonly role: string | null;\n readonly name: string | null;\n\n /**\n * Accessible description — what `aria-describedby` and `title` resolve to.\n *\n * Captured because nothing else captures it, and its absence was a hole. The\n * attribute allowlist drops every `aria-*` attribute on the stated grounds\n * that they \"are resolved into role/name/state\" — true of `aria-label` and\n * `aria-selected`, and false of `aria-describedby`, which resolves into a\n * description and had nowhere to land. A field whose `aria-describedby`\n * pointed at a deleted error message compared **equal**: the reference was\n * dropped by the allowlist, the name was unaffected, and the regression was\n * silent on every tier including raster.\n */\n readonly description?: string | null;\n\n /** `checked`, `disabled`, `expanded`, `selected`, … */\n readonly state: Readonly<Record<string, string | boolean | number>>;\n}\n\nexport interface MatchedRule {\n /** Identifies the origin sheet for attribution: href, or a synthetic id. */\n readonly sheet: string;\n readonly selector: string;\n /** `[idCount, classCount, typeCount]` — CSS specificity, most significant first. */\n readonly specificity: readonly [number, number, number];\n /** Document order of the rule. Breaks specificity ties, per the cascade. */\n readonly order: number;\n readonly declarations: readonly Declaration[];\n /** Source position, when the sheet exposes one. Turns a diff into a file:line. */\n readonly source?: { readonly file: string; readonly line: number };\n}\n\nexport interface Declaration {\n readonly property: string;\n readonly value: string;\n readonly important: boolean;\n /** Custom properties referenced by the value, for token-keyed attribution. */\n readonly references?: readonly string[];\n}\n\n/** Browser-layout coordinates retained as evidence, without a preferred layout policy. */\nexport interface Rect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\nexport interface Diagnostic {\n readonly severity: 'warn' | 'error';\n readonly code: string;\n readonly message: string;\n readonly nodePath?: string;\n}\n\n/**\n * One list from several, with a diagnostic said once however many times it was\n * found.\n *\n * A subject is described by a document, a snapshot and a comparison, and the\n * same condition is visible to more than one of them -- a missing font is in the\n * document that declared it and in the snapshot that measured it. A reader asks\n * \"what is wrong with this subject\" once, and a list that answers twice reads as\n * two findings.\n *\n * Identity is every field, `nodePath` included: the same code on two nodes is\n * two facts, and collapsing them would report one element where the page has a\n * dozen.\n */\nexport function mergeDiagnostics(\n ...lists: readonly (readonly Diagnostic[] | undefined)[]\n): readonly Diagnostic[] {\n const byIdentity = new Map<string, Diagnostic>();\n for (const list of lists) {\n for (const diagnostic of list ?? []) {\n const key = JSON.stringify([\n diagnostic.severity,\n diagnostic.code,\n diagnostic.message,\n diagnostic.nodePath ?? null,\n ]);\n if (!byIdentity.has(key)) byIdentity.set(key, diagnostic);\n }\n }\n return [...byIdentity.values()];\n}\n"]}
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/format/capture.ts"],"names":[],"mappings":"AAgQA,0EAA0E;AAC1E,MAAM,UAAU,gBAAgB,CAAC,CAAO,EAAE,CAAO;IAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,OAAO,KAAK,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACtD,CAAC;AASD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAG,KAAqD;IAExD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAC;IACjD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,UAAU,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;YACpC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;gBACzB,UAAU,CAAC,QAAQ;gBACnB,UAAU,CAAC,IAAI;gBACf,UAAU,CAAC,OAAO;gBAClB,UAAU,CAAC,QAAQ,IAAI,IAAI;aAC5B,CAAC,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;AAClC,CAAC","sourcesContent":["import type { EnvironmentInputs } from './environment.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The collector/core boundary.\n *\n * Collectors **extract**; `core` **normalizes**. Nothing in this file is a live\n * object — a `RawCapture` is plain data, fully serializable, produced by touching\n * a DOM and consumed by code that has never seen one.\n *\n * Two properties fall out of that split, and both are the reason for it:\n *\n * 1. **One ruleset, by construction.** Normalization is the moat (ADR-0003) and\n * the thing most likely to drift between two implementations. If the JSDOM and\n * Chromium collectors each normalized their own captures, \"the same rules ran\"\n * would be a claim maintained by discipline. Here it is a fact of the call\n * graph: there is one normalizer and both profiles enter it.\n * 2. **The sub-renderer can be anywhere.** A capture crossing a worker boundary,\n * a pipe, or a network hop to a device farm is the same value it was in\n * process. No code may assume the collector is local.\n */\nexport interface RawCapture {\n readonly captureVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n\n /**\n * Environment inputs the *collector* is positioned to know — engine version,\n * fonts actually loaded, resolved conditions, asset hashes. The ruleset and\n * allowlist versions are `core`'s to supply, so they are absent here and the\n * key is completed during normalization.\n */\n readonly environment: Omit<EnvironmentInputs, 'ruleset' | 'allowlist'>;\n\n readonly root: RawNode;\n\n /**\n * Inherited values in force at the subject root.\n *\n * Mandatory, not an optimization. CSS applicability pruning (ADR-0003) drops\n * every rule that matches nothing inside the subtree — including rules on\n * ancestors *outside* it whose inheritable properties still reach in. Without\n * this seed the cheap tier is unsound and will report false `unchanged`.\n */\n readonly inheritedSeed: Readonly<Record<string, string>>;\n\n /**\n * Subtrees this subject renders through portals, in fiber traversal order.\n *\n * A subject's boundary is a component-tree question, not a DOM-containment\n * one: `createPortal` renders elsewhere in the document while remaining part\n * of the tree rooted here. Omitting these makes an opening modal report\n * `unchanged`, because its own container is byte-identical (ADR-0007).\n */\n readonly portals?: readonly RawNode[];\n\n /**\n * Shared root state this subject is *latently* coupled to.\n *\n * Rules like `html.dark .card` contribute nothing today and everything the\n * moment `dark` lands on `<html>`. In a session that reuses one document across\n * subjects, that moment may arrive because a *different* subject put it there —\n * turning this subject's baseline order-dependent with no visible cause.\n * Recorded so the coupling can be reported before it bites.\n */\n readonly couplings?: readonly string[];\n\n /**\n * The document's base URL, `document.baseURI`, as the page reported it.\n *\n * An engine resolves every `url()` in a computed value against this, so a\n * build served on a different port comes back with different strings. The\n * normalizer drops this origin from them. Absent when the collector had no\n * page to ask, and then nothing is dropped.\n */\n readonly baseUrl?: string;\n\n /** Anything the collector could not do. Empty is the expected case. */\n readonly diagnostics: readonly Diagnostic[];\n}\n\nexport interface SubjectRef {\n /** Stable across renames of the file, e.g. `story:components-button--primary`. */\n readonly id: string;\n\n /**\n * What kind of thing this is, which is not the same question as what produced\n * it.\n *\n * `value` is the one that is not a rendering: a JSON body, a schema, a route\n * table. It is a fourth member rather than a reuse of `route`, which already\n * means *a page rendered at a URL* — a route table is a value about the same\n * paths, and a subject list where one word meant both is one nobody can filter.\n */\n readonly kind: 'story' | 'route' | 'fixture' | 'value';\n readonly title?: string;\n}\n\n/**\n * A node exactly as observed, before any rule is applied.\n *\n * Raw means raw: `attributes` still holds generated ids and hashed class names,\n * `matchedRules` still holds cascade losers. Normalization needs the unedited\n * input — a collector that helpfully pre-cleaned would be a second, invisible\n * ruleset, versioned by nothing.\n */\nexport interface RawNode {\n /**\n * Lower-case element name, `#text` for a text run, or `::before` / `::after`\n * for a box CSS generates inside its parent — children with no DOM node,\n * which paint like any other child.\n */\n readonly tag: string;\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Resolved accessibility properties, when the profile provides them. */\n readonly aria?: RawAria;\n\n /**\n * Rules matching this node, as authored.\n *\n * Losing declarations are retained: `core` resolves the cascade for profiles\n * whose engine did not (ADR-0003 step 5), and cannot do that from winners\n * alone. Shorthands are retained *unexpanded* — expansion is `core`'s job, so\n * that it is versioned by the ruleset rather than by each collector.\n *\n * A collector MAY drop a declaration whose property is neither allowlisted nor\n * a shorthand of an allowlisted property, and SHOULD when the capture crosses\n * a network hop. It MUST NOT normalize values; a helpfully pre-cleaned capture\n * is a second, invisible ruleset versioned by nothing.\n */\n readonly matchedRules: readonly MatchedRule[];\n\n readonly inlineStyle?: Readonly<Record<string, string>>;\n\n /** Engine-resolved computed style. Present only when `profile.computedStyle`. */\n readonly computedStyle?: Readonly<Record<string, string>>;\n\n /** Present only when `profile.layout`. */\n readonly rect?: Rect;\n\n /** Literal text of a text node. Digested during normalization, per policy. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when an adapter supplied it.\n *\n * Beside `provenance` because it arrives from the same seam and under the same\n * rule: the collector carries no framework dependency and both are injected.\n * See {@link Wiring} for why it is a dimension rather than another attribution\n * field.\n */\n readonly wiring?: Wiring;\n\n /**\n * What the component that authored this node was holding, when an adapter\n * supplied it.\n *\n * A third injection at the same seam, and deliberately not folded into\n * `wiring`: wiring is hashed into a band and this may never be, so a caller\n * that wants the band must be able to have it without also carrying values,\n * and a caller that wants explanations must be able to ask for them without\n * changing a single digest. See {@link Holding}.\n */\n readonly holding?: Holding;\n\n readonly children: readonly RawNode[];\n\n /** Nodes inside a shadow root, kept distinct from light-DOM children. */\n readonly shadowChildren?: readonly RawNode[];\n\n /**\n * Ignore rules the collector found this element under (spec 0024).\n *\n * A mark, not a removal. The collector is the only thing holding a live DOM, so\n * it is the only thing that can run a selector or read a marker attribute — but\n * it must not act on what it finds. An element deleted from the capture would\n * be invisible to every count downstream, and the difference between \"absorbed\n * by the `carousel` rule\" and \"was never there\" is the difference between an\n * ignore and a blind spot.\n *\n * Never hashed. Normalization turns these into\n * {@link import('./snapshot.js').SemanticSnapshot.ignoreSites}, which sits\n * beside `styleProvenance` outside the render hash for the same reason: masking\n * a clock changes what a run *says*, not what it *renders*, and must not\n * re-baseline the repository.\n */\n readonly ignoredBy?: readonly string[];\n\n /**\n * Why part of this node was not read, on a node whose existence is known and\n * whose values are not — a `::before` whose `content` holds a `counter()` under\n * a host with no layout engine.\n *\n * A marker, never a value: the fields it describes are absent rather than\n * guessed (ADR-0002). Outside every hash, like `ignoredBy` above it.\n */\n readonly unread?: string;\n}\n\nexport interface RawAria {\n readonly role: string | null;\n readonly name: string | null;\n\n /**\n * Accessible description — what `aria-describedby` and `title` resolve to.\n *\n * Captured because nothing else captures it, and its absence was a hole. The\n * attribute allowlist drops every `aria-*` attribute on the stated grounds\n * that they \"are resolved into role/name/state\" — true of `aria-label` and\n * `aria-selected`, and false of `aria-describedby`, which resolves into a\n * description and had nowhere to land. A field whose `aria-describedby`\n * pointed at a deleted error message compared **equal**: the reference was\n * dropped by the allowlist, the name was unaffected, and the regression was\n * silent on every tier including raster.\n */\n readonly description?: string | null;\n\n /** `checked`, `disabled`, `expanded`, `selected`, … */\n readonly state: Readonly<Record<string, string | boolean | number>>;\n}\n\nexport interface MatchedRule {\n /** Identifies the origin sheet for attribution: href, or a synthetic id. */\n readonly sheet: string;\n readonly selector: string;\n /** `[idCount, classCount, typeCount]` — CSS specificity, most significant first. */\n readonly specificity: readonly [number, number, number];\n /** Document order of the rule. Breaks specificity ties, per the cascade. */\n readonly order: number;\n readonly declarations: readonly Declaration[];\n /** Source position, when the sheet exposes one. Turns a diff into a file:line. */\n readonly source?: { readonly file: string; readonly line: number };\n}\n\nexport interface Declaration {\n readonly property: string;\n readonly value: string;\n readonly important: boolean;\n /** Custom properties referenced by the value, for token-keyed attribution. */\n readonly references?: readonly string[];\n}\n\n/** Browser-layout coordinates retained as evidence, without a preferred layout policy. */\nexport interface Rect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/** The area two rects share; zero when they only touch or do not meet. */\nexport function intersectionArea(a: Rect, b: Rect): number {\n const width = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);\n const height = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);\n return width > 0 && height > 0 ? width * height : 0;\n}\n\nexport interface Diagnostic {\n readonly severity: 'warn' | 'error';\n readonly code: string;\n readonly message: string;\n readonly nodePath?: string;\n}\n\n/**\n * One list from several, with a diagnostic said once however many times it was\n * found.\n *\n * A subject is described by a document, a snapshot and a comparison, and the\n * same condition is visible to more than one of them -- a missing font is in the\n * document that declared it and in the snapshot that measured it. A reader asks\n * \"what is wrong with this subject\" once, and a list that answers twice reads as\n * two findings.\n *\n * Identity is every field, `nodePath` included: the same code on two nodes is\n * two facts, and collapsing them would report one element where the page has a\n * dozen.\n */\nexport function mergeDiagnostics(\n ...lists: readonly (readonly Diagnostic[] | undefined)[]\n): readonly Diagnostic[] {\n const byIdentity = new Map<string, Diagnostic>();\n for (const list of lists) {\n for (const diagnostic of list ?? []) {\n const key = JSON.stringify([\n diagnostic.severity,\n diagnostic.code,\n diagnostic.message,\n diagnostic.nodePath ?? null,\n ]);\n if (!byIdentity.has(key)) byIdentity.set(key, diagnostic);\n }\n }\n return [...byIdentity.values()];\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import type { Diagnostic, SubjectRef } from './capture.js';
2
2
  import type { Viewport } from './environment.js';
3
3
  import { type Digest } from './hash.js';
4
+ import type { PackedSnapshot } from './packed.js';
4
5
  import type { ComponentHash } from './snapshot.js';
5
6
  /**
6
7
  * The render document: what gets *sent* somewhere to become an image.
@@ -127,8 +128,9 @@ export interface RenderFrame {
127
128
  /** Elements between `<body>` and the subject root, outermost first. */
128
129
  readonly ancestors: readonly FrameElement[];
129
130
  /**
130
- * Used width of the subject's parent box, in CSS pixels, when layout was
131
- * observable.
131
+ * Used `width` of the subject's parent, in CSS pixels and in the parent's own
132
+ * `box-sizing`, when layout was observable. Written back on the same box, it
133
+ * gives the content box a percentage on the subject resolves against.
132
134
  *
133
135
  * A subject declaring `width: 100%` is a different size in a 1024px page and
134
136
  * inside a 600px panel, and nothing else in this document says which it was.
@@ -198,8 +200,8 @@ export interface RenderIdentity {
198
200
  * The gap itself is acceptable; a *difference* in it between two runs is not,
199
201
  * because the images then differ for a reason that is not the code and the
200
202
  * report blames whichever component sits under the pixels. Folding it in here
201
- * makes that comparison `incomparable` instead, by the same mechanism that
202
- * refuses a cross-machine one.
203
+ * names that gap as the cause of the change instead, by the same mechanism
204
+ * that names a moved machine.
203
205
  *
204
206
  * Optional only because a renderer may predate the field. Absent means the
205
207
  * renderer did not record what it did, which is emphatically not the same as
@@ -261,7 +263,7 @@ export interface Raster {
261
263
  * change from the components the change merely moved needs both revisions, and
262
264
  * a stored baseline is an image: without this, a run has one document and ranks
263
265
  * by area — which
264
- * [journal 0013](../../../../docs/context/journal/0013-observability.md)
266
+ * [journal 0013](https://github.com/Variance-Authority/variance-authority/blob/main/docs/context/journal/0013-observability.md)
265
267
  * measured as backwards.
266
268
  *
267
269
  * Text, and small: one line per component boundary. It rides in the sidecar
@@ -298,6 +300,27 @@ export interface Raster {
298
300
  * defect in the suite as newly introduced on the first run after an upgrade.
299
301
  */
300
302
  readonly findingMarks?: readonly string[];
303
+ /**
304
+ * The semantic snapshot of the document that painted this, packed.
305
+ *
306
+ * The region join needs to know where each element was in the render the
307
+ * pixels are compared against. A changed region is the union of what left and
308
+ * what arrived, and a stored baseline is an image: without its own tree, a
309
+ * control replaced by a larger element changes pixels outside every box this
310
+ * render has, and the join names the row around it. With it, the join pairs
311
+ * the two renders' nodes with the matcher the semantic diff reads, so a
312
+ * region and a delta agree about which element a node was.
313
+ *
314
+ * Packed (see {@link PackedSnapshot}) because the tree is an order of
315
+ * magnitude larger than the component hashes, and it rides in every sidecar.
316
+ * Every store and transport carries the text as it is, and only the run that
317
+ * decides unpacks it.
318
+ *
319
+ * **Absent means not recorded.** A baseline written before this existed, or
320
+ * from a collector that supplied no snapshot, has none, and a verdict whose
321
+ * regions were joined without it says so.
322
+ */
323
+ readonly snapshot?: PackedSnapshot;
301
324
  }
302
325
  /**
303
326
  * A raster whose image is in hand.
@@ -1,4 +1,5 @@
1
1
  import { digestValue } from './hash.js';
2
+ import { pageOrigin, withoutOrigin } from './origin.js';
2
3
  /**
3
4
  * Content address of a document: *what is to be painted*.
4
5
  *
@@ -21,6 +22,7 @@ import { digestValue } from './hash.js';
21
22
  * never a wrong image.
22
23
  */
23
24
  export function documentDigest(document) {
25
+ const origin = pageOrigin(document.baseUrl);
24
26
  return digestValue({
25
27
  documentVersion: document.documentVersion,
26
28
  html: document.html,
@@ -40,11 +42,15 @@ export function documentDigest(document) {
40
42
  inherited: { ...document.inherited },
41
43
  fonts: [...document.fonts],
42
44
  ...(document.assets ? { assets: { ...document.assets } } : {}),
43
- ...(document.baseUrl !== undefined ? { baseUrl: document.baseUrl } : {}),
45
+ // The base and the page's own resources as paths: a collector serves the
46
+ // build on a free port, and a digest holding it would miss the render cache
47
+ // on every run. The document keeps both absolute, because the renderer
48
+ // resolves against them; a resource another origin serves stays absolute.
49
+ ...(document.baseUrl !== undefined ? { baseUrl: withoutOrigin(document.baseUrl, origin) } : {}),
44
50
  ...(document.resources !== undefined
45
51
  ? {
46
52
  resources: Object.fromEntries(Object.entries(document.resources).map(([url, resource]) => [
47
- url,
53
+ withoutOrigin(url, origin),
48
54
  { ...resource },
49
55
  ])),
50
56
  }