@variance-authority/core 0.15.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 (82) hide show
  1. package/CHANGELOG.md +424 -0
  2. package/README.md +25 -7
  3. package/dist/attribute/component-hash.d.ts +11 -3
  4. package/dist/attribute/component-hash.js +11 -3
  5. package/dist/attribute/component-hash.js.map +1 -1
  6. package/dist/attribute/movement.d.ts +1 -1
  7. package/dist/attribute/movement.js.map +1 -1
  8. package/dist/attribute/region.d.ts +47 -4
  9. package/dist/attribute/region.js +90 -17
  10. package/dist/attribute/region.js.map +1 -1
  11. package/dist/attribute/source.d.ts +1 -1
  12. package/dist/attribute/source.js +1 -1
  13. package/dist/attribute/source.js.map +1 -1
  14. package/dist/compare/band.d.ts +8 -2
  15. package/dist/compare/band.js +7 -2
  16. package/dist/compare/band.js.map +1 -1
  17. package/dist/compare/diff/attribution.js +6 -4
  18. package/dist/compare/diff/attribution.js.map +1 -1
  19. package/dist/compare/diff/compare-nodes.js +9 -1
  20. package/dist/compare/diff/compare-nodes.js.map +1 -1
  21. package/dist/compare/diff/match.d.ts +2 -1
  22. package/dist/compare/diff/match.js +52 -0
  23. package/dist/compare/diff/match.js.map +1 -1
  24. package/dist/compare/parting.js +6 -4
  25. package/dist/compare/parting.js.map +1 -1
  26. package/dist/format/document.d.ts +26 -3
  27. package/dist/format/document.js.map +1 -1
  28. package/dist/format/index.d.ts +7 -0
  29. package/dist/format/index.js +4 -0
  30. package/dist/format/index.js.map +1 -1
  31. package/dist/format/packed.d.ts +32 -0
  32. package/dist/format/packed.js +23 -0
  33. package/dist/format/packed.js.map +1 -0
  34. package/dist/format/shape.d.ts +31 -0
  35. package/dist/format/shape.js +269 -0
  36. package/dist/format/shape.js.map +1 -0
  37. package/dist/format/stabilize.d.ts +7 -1
  38. package/dist/format/stabilize.js +10 -3
  39. package/dist/format/stabilize.js.map +1 -1
  40. package/dist/format/steps.d.ts +44 -0
  41. package/dist/format/steps.js +67 -0
  42. package/dist/format/steps.js.map +1 -0
  43. package/dist/format/width.d.ts +35 -0
  44. package/dist/format/width.js +38 -0
  45. package/dist/format/width.js.map +1 -0
  46. package/dist/judge/docket.js +1 -0
  47. package/dist/judge/docket.js.map +1 -1
  48. package/dist/judge/index.d.ts +1 -0
  49. package/dist/judge/index.js +1 -0
  50. package/dist/judge/index.js.map +1 -1
  51. package/dist/judge/restamp.d.ts +20 -0
  52. package/dist/judge/restamp.js +20 -0
  53. package/dist/judge/restamp.js.map +1 -0
  54. package/dist/relate/before.js +2 -2
  55. package/dist/relate/before.js.map +1 -1
  56. package/dist/relate/carried.d.ts +1 -1
  57. package/dist/relate/carried.js +5 -5
  58. package/dist/relate/carried.js.map +1 -1
  59. package/dist/relate/graph.d.ts +14 -12
  60. package/dist/relate/graph.js +12 -11
  61. package/dist/relate/graph.js.map +1 -1
  62. package/dist/relate/index.d.ts +1 -2
  63. package/dist/relate/index.js +1 -2
  64. package/dist/relate/index.js.map +1 -1
  65. package/dist/relate/reach.d.ts +5 -5
  66. package/dist/relate/reach.js.map +1 -1
  67. package/dist/relate/records.d.ts +5 -4
  68. package/dist/relate/records.js +3 -2
  69. package/dist/relate/records.js.map +1 -1
  70. package/dist/rules/normalize/fixture.d.ts +2 -0
  71. package/dist/rules/normalize/fixture.js +2 -1
  72. package/dist/rules/normalize/fixture.js.map +1 -1
  73. package/dist/shard/placement.d.ts +10 -8
  74. package/dist/shard/placement.js +7 -21
  75. package/dist/shard/placement.js.map +1 -1
  76. package/dist/share/index.d.ts +1 -1
  77. package/dist/share/index.js +1 -1
  78. package/dist/share/index.js.map +1 -1
  79. package/package.json +2 -2
  80. package/dist/relate/dominate.d.ts +0 -19
  81. package/dist/relate/dominate.js +0 -77
  82. package/dist/relate/dominate.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"component-hash.js","sourceRoot":"","sources":["../../src/attribute/component-hash.ts"],"names":[],"mappings":"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","sourcesContent":["import { BANDS, type Band } from '../compare/band.js';\nimport { declaredIn } from '../compare/cascade.js';\nimport type { CanonicalValue } from '../format/canonical.js';\nimport type { Rect } from '../format/capture.js';\nimport type { Digest } from '../format/hash.js';\nimport { digestValue } from '../format/hash.js';\nimport type { ComponentHash, SemanticSnapshot } from '../format/snapshot.js';\nimport { boundaries, shapeOf, UNATTRIBUTED } from './boundary.js';\n\nexport type { ComponentHash } from '../format/snapshot.js';\nexport { UNATTRIBUTED } from './boundary.js';\n\n/**\n * The digests a comparison of two component states actually reads.\n *\n * `movedBands` used to take two `ComponentHash`es, which carry a name and an\n * instance count neither side of the comparison consults. Narrowing it to the\n * digests is what lets a per-instance record (`ComponentInstance`, which has no\n * `instances` count because it *is* one) be compared by the same function —\n * rather than by a second copy of the band mapping, which is the one thing in\n * this file that must not exist twice: a subject relaxed to `layout` has to\n * absorb the same bands whichever shape the caller happened to be holding.\n */\nexport interface BandDigests {\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n readonly geometry?: Digest;\n}\n\n/**\n * Per-component content hashes, one per band.\n *\n * `SemanticSnapshot` hashes a whole subject. That answers \"did anything change\"\n * and nothing else: it moves whenever anything inside it moves, so it cannot say\n * *which area*, and a record built on it says \"the page changed\" on every commit.\n *\n * These hashes are the unit a history is kept in. Text, and small — a component\n * that did not change contributes the same digest it did last time, so the only\n * thing worth recording is the difference.\n */\n\n/**\n * Hash every component boundary in a subject.\n *\n * Ordered by component name so an unchanged subject produces byte-identical\n * output across runs.\n *\n * The walk and the per-boundary shape live in {@link ./boundary.js}, which\n * {@link ./instances.js} shares — one definition of what a component's own\n * content is, read by the aggregate and by the per-instance form.\n */\nexport function hashComponents(snapshot: SemanticSnapshot): readonly ComponentHash[] {\n const layout = snapshot.profile.layout;\n const declared = declaredIn(snapshot);\n\n const accumulated = new Map<\n string,\n {\n structure: CanonicalValue[];\n semantics: CanonicalValue[];\n text: CanonicalValue[];\n style: CanonicalValue[];\n geometry: CanonicalValue[];\n boxes: (Rect | null)[];\n values: Map<string, Set<string>>;\n }\n >();\n\n for (const boundary of boundaries(snapshot.root)) {\n const shape = shapeOf(boundary, layout, undefined, declared);\n\n const entry = accumulated.get(boundary.component) ?? {\n structure: [],\n semantics: [],\n text: [],\n style: [],\n geometry: [],\n boxes: [],\n values: new Map<string, Set<string>>(),\n };\n entry.structure.push(shape.structure);\n entry.semantics.push(shape.semantics);\n entry.text.push(shape.text);\n entry.style.push(shape.style);\n entry.geometry.push(shape.geometry);\n entry.boxes.push(shape.box);\n for (const [property, values] of Object.entries(shape.values ?? {})) {\n const seen = entry.values.get(property) ?? new Set<string>();\n for (const value of values) seen.add(value);\n entry.values.set(property, seen);\n }\n accumulated.set(boundary.component, entry);\n }\n\n return [...accumulated.entries()]\n .map(([component, entry]) => ({\n component,\n instances: entry.structure.length,\n structure: digestValue(entry.structure),\n semantics: digestValue(entry.semantics),\n text: digestValue(entry.text),\n style: digestValue(entry.style),\n ...(layout ? { geometry: digestValue(entry.geometry) } : {}),\n // Under a profile with no layout every entry is `null`, and a list of\n // nulls is a measurement nobody took written as one they did. Absent\n // instead, which is the same rule `geometry` above obeys.\n ...(layout ? { boxes: entry.boxes } : {}),\n values: sortedValues(entry.values),\n }))\n // Code-unit order, not `localeCompare`. The doc above promises byte-identical\n // output for an unchanged subject, and a locale-aware comparison makes that a\n // promise about the machine's `LANG` — which was tolerable while these were\n // internal and is not now that they are written into a sidecar, committed\n // beside a baseline, and read back on someone else's runner.\n .sort((a, b) => byCodeUnit(a.component, b.component));\n}\n\n/** Properties and their values in code-unit order, so the sidecar is byte-stable. */\nfunction sortedValues(values: ReadonlyMap<string, ReadonlySet<string>>): Record<string, string[]> {\n const sorted: Record<string, string[]> = {};\n for (const property of [...values.keys()].sort(byCodeUnit)) {\n sorted[property] = [...values.get(property)!].sort(byCodeUnit);\n }\n return sorted;\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/**\n * Which components *caused* a change, given both revisions' hashes.\n *\n * The arithmetic behind cause-first ranking, and the reason a baseline carries\n * its component hashes at all. `rankRegions` takes a list of cause names and,\n * given none, falls back to area — which\n * [journal 0013](../../../../docs/context/journal/0013-observability.md)\n * measured as backwards by 6×, because area measures displacement rather than\n * cause. This is where that list comes from on a path with no second document.\n *\n * The distinction is the split ADR-0018 built the bands for:\n *\n * - **`structure` or `style` moved** — the component's own content is different.\n * It is a cause.\n * - **only `geometry` moved** — the component is byte-identical and its box is\n * somewhere else. Something *else* moved it, so it is collateral, and naming\n * it would send a reviewer to a file nobody edited.\n * - **appeared or disappeared** — a component present on one side only is a\n * cause. Something decided to render it or to stop.\n *\n * `instances` is deliberately not consulted. A component rendered five times\n * instead of four has a different `structure` digest for the subject, and the\n * count moving on its own — the same component, the same content, one more of\n * them — is a change in whatever decided how many, not in this component.\n *\n * A profile with no layout supplies no `geometry`, so on that tier every\n * difference is `structure` or `style` and every changed component is a cause.\n * That is correct rather than degraded: with no boxes, nothing was displaced.\n */\nexport function causesBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly string[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const causes: string[] = [];\n\n for (const entry of after) {\n // `(unattributed)` is not a component and can never be a cause. It is the\n // bucket for nodes whose provenance chain broke, so it collects unrelated\n // parts of a page under one name — and nothing downstream could act on it\n // anyway: a region with no owner reports no component, so it would never\n // match. A broken chain is a defect in this tool and is reported as\n // `unattributed` where that means something, not smuggled in here as a\n // culprit.\n if (entry.component === UNATTRIBUTED) continue;\n\n const was = previous.get(entry.component);\n if (was === undefined) {\n causes.push(entry.component);\n continue;\n }\n if (ownContentMoved(was, entry)) causes.push(entry.component);\n }\n\n // Removals too, and they are the case a candidate-only walk cannot see: a\n // component that stopped rendering leaves regions behind it, and the component\n // that used to be there is exactly the name a reviewer needs.\n const present = new Set(after.map((entry) => entry.component));\n for (const entry of before) {\n if (entry.component === UNATTRIBUTED) continue;\n if (!present.has(entry.component)) causes.push(entry.component);\n }\n\n return causes.sort();\n}\n\n/**\n * Whether a component's *own content* differs, ignoring where its box ended up.\n *\n * Deliberately not phrased in bands, because it is not a band question. Both\n * `structure` and `geometry` map to the `geometry` band, and this has to keep\n * them apart: a component whose tree changed edited itself, and a component\n * whose rect moved was pushed. That distinction is the entire cause/collateral\n * result, and asking it through the band mapping would need the digests back\n * again to answer it.\n *\n * The list is every digest a component owns except `geometry`. It grew by two on\n * 2026-08-06 without changing meaning: `semantics` and `text` used to be inside\n * `structure`.\n */\nfunction ownContentMoved(before: BandDigests, after: BandDigests): boolean {\n return (\n before.structure !== after.structure ||\n before.semantics !== after.semantics ||\n before.text !== after.text ||\n before.style !== after.style\n );\n}\n\n/**\n * Which frequency bands moved between one component's two hashes.\n *\n * The reason the digests were split. A baseline carries hashes and not\n * documents, so \"what changed here\" used to be answerable only as a boolean —\n * and a boolean cannot serve a route-level test, whose entire request is *tell\n * me when the page stops assembling and never when it is repainted*.\n *\n * The mapping is exact and it is the same one `bandOf` applies to a delta, which\n * is the property that matters: a subject relaxed to `layout` must absorb the\n * same things whether the run held two documents or two sidecars. Two mappings\n * would be one drift away from a config key meaning different things on the two\n * paths, discovered as a regression somebody let through.\n *\n * | digest | band | what it covers |\n * |---|---|---|\n * | `semantics` | `a11y` | role, accessible name, ARIA state |\n * | `text` | `content` | text runs |\n * | `structure` | `geometry` | tags, aliases, attributes, child boundaries |\n * | `geometry` | `geometry` | rects and computed layout output |\n * | `style` | `token` | declared values and custom properties |\n *\n * `texture` never appears. It is raster residue by definition, and a component\n * hash is built from a document — so the band a comparison of hashes cannot\n * decide is *absent* from the answer rather than reported as unmoved, which is\n * ADR-0002's rule applied to a narrower question.\n *\n * A missing `geometry` on either side is the profile saying it has no layout\n * engine, and is not a difference. Treating absent as a change would report\n * every component as having moved the moment a jsdom baseline met a Chromium\n * run — which the environment key already names as the cause of that\n * comparison, so this would be a second, wronger answer to a question already\n * settled.\n */\nexport function movedBands(before: BandDigests, after: BandDigests): readonly Band[] {\n const moved = new Set<Band>();\n\n if (before.semantics !== after.semantics) moved.add('a11y');\n if (before.text !== after.text) moved.add('content');\n if (before.structure !== after.structure) moved.add('geometry');\n if (before.style !== after.style) moved.add('token');\n if (\n before.geometry !== undefined &&\n after.geometry !== undefined &&\n before.geometry !== after.geometry\n ) {\n moved.add('geometry');\n }\n\n return BANDS.filter((band) => moved.has(band));\n}\n\n/**\n * Every band that moved anywhere in the subject, given both revisions' hashes.\n *\n * A component present on one side only contributes `geometry`: something was\n * added or removed, which is the structural half of that band however the rest\n * of it compares. It deliberately does not contribute `a11y` or `content` as\n * well — a component that is simply not there did not *rename* anything, and\n * inflating the answer would make a level that absorbs nothing look like the\n * only safe choice.\n */\nexport function bandsBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly Band[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const present = new Set(after.map((entry) => entry.component));\n const moved = new Set<Band>();\n\n for (const entry of after) {\n const was = previous.get(entry.component);\n if (was === undefined) moved.add('geometry');\n else for (const band of movedBands(was, entry)) moved.add(band);\n }\n\n for (const entry of before) {\n if (!present.has(entry.component)) moved.add('geometry');\n }\n\n return BANDS.filter((band) => moved.has(band));\n}\n\n/**\n * Which bands moved, kept per component instead of folded into one list.\n *\n * {@link bandsBetween} answers *what kind of change is in this subject*, which is\n * the question a sensitivity level asks. It cannot answer the one a reviewer\n * asks — *what changed, and where* — because the fold is lossy in exactly the\n * place attribution lives: a subject reporting `content, geometry, token` has\n * told you a colour and a string and a size all moved somewhere in it, and left\n * you to guess which of the forty components on the page owns which.\n *\n * Unfolded, the same two sidecars say `Button — geometry, token` and\n * `CardFooter — a11y, content`, and that is a sentence a page can print beside a\n * picture. It is also the record that survives when the raster tier loses the\n * name: a difference that reflowed its neighbours merges into one blob whose box\n * fits no component, so the region resolves to the document root and the edit\n * arrives unattributed — while the hashes, which never looked at a pixel, still\n * hold the component that moved and the sense in which it moved.\n *\n * `cause` repeats the {@link causesBetween} predicate rather than being derived\n * from `bands`, and the repetition is the point: `structure` and `geometry` both\n * map to the `geometry` band, so a component that edited its own tree and one\n * that was merely pushed by a neighbour are indistinguishable *after* the band\n * mapping. Losing that here would make every reflowed container a culprit.\n */\nexport interface ComponentBands {\n readonly component: string;\n /** Non-empty: a component whose digests all matched has no entry at all. */\n readonly bands: readonly Band[];\n /** Its own content moved, as opposed to only its rect. `causesBetween`'s test. */\n readonly cause: boolean;\n /** Set only when the component is on one side alone, which `bands` cannot say. */\n readonly presence?: 'added' | 'removed';\n\n /**\n * How much bigger its own box got, when every instance agrees on the answer.\n *\n * The band a reviewer actually wanted. `geometry` says a rect under here is\n * not the rect it was; this says the control is eight pixels taller and\n * thirty-six wider, which is the padding somebody edited, arriving as a\n * measurement rather than as a guess about which property produced it.\n *\n * Absent for four different reasons and they are one reason: nobody can say.\n * No boxes on one side, a different number of instances, instances that\n * disagree about the delta, or a box that did not change size. Present is\n * always non-zero on at least one axis.\n */\n readonly grew?: { readonly width: number; readonly height: number };\n\n /**\n * Which declared values changed, by property, when both sides recorded them.\n *\n * `token` says a declaration under here moved; this names it. `from` holds the\n * values only the baseline had and `to` the values only this run has, so a\n * component rendered as primary and secondary that changed one background\n * reports that background and not the other. An empty side is a property no\n * node declared on that side, which was measured and is not the same as absent.\n *\n * Absent when either sidecar predates the record, and when the style digest\n * moved but no property's set of values did — a value that moved from one\n * node to another is a real change this cannot describe, and an empty list\n * would say there was none. Present is never empty.\n */\n readonly changed?: readonly ValueChange[];\n}\n\n/** One property whose set of declared values differs between two revisions. */\nexport interface ValueChange {\n readonly property: string;\n readonly from: readonly string[];\n readonly to: readonly string[];\n}\n\n/**\n * Every component whose hashes differ, with the bands it differs in.\n *\n * One-sided components contribute `geometry` and nothing else, which is\n * {@link bandsBetween}'s rule and must stay identical to it: a component that is\n * simply not there did not rename anything, and inflating the answer would put\n * an `a11y` claim on a page for a component nobody can look at. `presence` is\n * what carries the rest of that meaning, so no reader has to infer *appeared*\n * from a lone `geometry`.\n *\n * `(unattributed)` is excluded for the reason {@link causesBetween} excludes it:\n * it is a bucket for nodes whose provenance chain broke, so it collects\n * unrelated parts of a page under one name and nothing downstream could act on\n * it. The broken chain is reported where it means something, not here as a\n * component that moved.\n */\nexport function movedBandsBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly ComponentBands[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const present = new Set(after.map((entry) => entry.component));\n const moved: ComponentBands[] = [];\n\n for (const entry of after) {\n if (entry.component === UNATTRIBUTED) continue;\n const was = previous.get(entry.component);\n if (was === undefined) {\n moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'added' });\n continue;\n }\n const bands = movedBands(was, entry);\n if (bands.length === 0) continue;\n const grew = grewBetween(was.boxes, entry.boxes);\n const changed = was.style === entry.style ? undefined : changedBetween(was.values, entry.values);\n moved.push({\n component: entry.component,\n bands,\n cause: ownContentMoved(was, entry),\n ...(grew === undefined ? {} : { grew }),\n ...(changed === undefined ? {} : { changed }),\n });\n }\n\n for (const entry of before) {\n if (entry.component === UNATTRIBUTED) continue;\n if (present.has(entry.component)) continue;\n moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'removed' });\n }\n\n return moved.sort((left, right) => byCodeUnit(left.component, right.component));\n}\n\n/**\n * Every property whose values differ, or nothing when that cannot be said.\n *\n * Read only when the `style` digest moved, which is the digest these values\n * were declared into: a component whose declarations hashed the same has\n * nothing here to name, whatever else about it changed.\n */\nfunction changedBetween(\n before: ComponentHash['values'],\n after: ComponentHash['values'],\n): readonly ValueChange[] | undefined {\n if (before === undefined || after === undefined) return undefined;\n\n const changed: ValueChange[] = [];\n const properties = new Set([...Object.keys(before), ...Object.keys(after)]);\n for (const property of [...properties].sort(byCodeUnit)) {\n const was = before[property] ?? [];\n const now = after[property] ?? [];\n const from = was.filter((value) => !now.includes(value));\n const to = now.filter((value) => !was.includes(value));\n if (from.length > 0 || to.length > 0) changed.push({ property, from, to });\n }\n return changed.length === 0 ? undefined : changed;\n}\n\n/**\n * One size delta both sides agree on, or nothing.\n *\n * Instances are paired by document order, which is the only order either side\n * has. That pairing is sound exactly while the counts match: a component that\n * gained an instance shifted every index after the insertion, and the deltas\n * that fell out would be measurements of one instance against a different one.\n * So a changed count answers nothing rather than answering wrongly — the count\n * itself already moved `structure`, and the reviewer is told that instead.\n *\n * Instances that disagree also answer nothing. Three buttons where one grew and\n * two did not is a real finding and it is not *this* one, and printing the first\n * or the largest would be the page picking a representative and not saying so.\n *\n * Position is deliberately not read. Everything below a control that got taller\n * moved down, and a delta drawn from `x`/`y` would name every one of them.\n */\nfunction grewBetween(\n before: readonly (Rect | null)[] | undefined,\n after: readonly (Rect | null)[] | undefined,\n): { readonly width: number; readonly height: number } | undefined {\n if (before === undefined || after === undefined) return undefined;\n if (before.length === 0 || before.length !== after.length) return undefined;\n\n let agreed: { width: number; height: number } | undefined;\n for (const [index, was] of before.entries()) {\n const now = after[index];\n if (was === null || was === undefined || now === null || now === undefined) return undefined;\n\n const width = now.width - was.width;\n const height = now.height - was.height;\n if (agreed === undefined) agreed = { width, height };\n else if (agreed.width !== width || agreed.height !== height) return undefined;\n }\n\n return agreed === undefined || (agreed.width === 0 && agreed.height === 0) ? undefined : agreed;\n}\n"]}
1
+ {"version":3,"file":"component-hash.js","sourceRoot":"","sources":["../../src/attribute/component-hash.ts"],"names":[],"mappings":"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","sourcesContent":["import { BANDS, type Band } from '../compare/band.js';\nimport { declaredIn } from '../compare/cascade.js';\nimport type { CanonicalValue } from '../format/canonical.js';\nimport type { Rect } from '../format/capture.js';\nimport type { Digest } from '../format/hash.js';\nimport { digestValue } from '../format/hash.js';\nimport type { ComponentHash, SemanticSnapshot } from '../format/snapshot.js';\nimport { boundaries, shapeOf, UNATTRIBUTED } from './boundary.js';\n\nexport type { ComponentHash } from '../format/snapshot.js';\nexport { UNATTRIBUTED } from './boundary.js';\n\n/**\n * The digests a comparison of two component states actually reads.\n *\n * `movedBands` used to take two `ComponentHash`es, which carry a name and an\n * instance count neither side of the comparison consults. Narrowing it to the\n * digests is what lets a per-instance record (`ComponentInstance`, which has no\n * `instances` count because it *is* one) be compared by the same function —\n * rather than by a second copy of the band mapping, which is the one thing in\n * this file that must not exist twice: a subject relaxed to `layout` has to\n * absorb the same bands whichever shape the caller happened to be holding.\n */\nexport interface BandDigests {\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n readonly geometry?: Digest;\n}\n\n/**\n * Per-component content hashes, one per band.\n *\n * `SemanticSnapshot` hashes a whole subject. That answers \"did anything change\"\n * and nothing else: it moves whenever anything inside it moves, so it cannot say\n * *which area*, and a record built on it says \"the page changed\" on every commit.\n *\n * These hashes are the unit a history is kept in. Text, and small — a component\n * that did not change contributes the same digest it did last time, so the only\n * thing worth recording is the difference.\n */\n\n/**\n * Hash every component boundary in a subject.\n *\n * Ordered by component name so an unchanged subject produces byte-identical\n * output across runs.\n *\n * The walk and the per-boundary shape live in {@link ./boundary.js}, which\n * {@link ./instances.js} shares — one definition of what a component's own\n * content is, read by the aggregate and by the per-instance form.\n */\nexport function hashComponents(snapshot: SemanticSnapshot): readonly ComponentHash[] {\n const layout = snapshot.profile.layout;\n const declared = declaredIn(snapshot);\n\n const accumulated = new Map<\n string,\n {\n structure: CanonicalValue[];\n semantics: CanonicalValue[];\n text: CanonicalValue[];\n style: CanonicalValue[];\n geometry: CanonicalValue[];\n boxes: (Rect | null)[];\n values: Map<string, Set<string>>;\n }\n >();\n\n for (const boundary of boundaries(snapshot.root)) {\n const shape = shapeOf(boundary, layout, undefined, declared);\n\n const entry = accumulated.get(boundary.component) ?? {\n structure: [],\n semantics: [],\n text: [],\n style: [],\n geometry: [],\n boxes: [],\n values: new Map<string, Set<string>>(),\n };\n entry.structure.push(shape.structure);\n entry.semantics.push(shape.semantics);\n entry.text.push(shape.text);\n entry.style.push(shape.style);\n entry.geometry.push(shape.geometry);\n entry.boxes.push(shape.box);\n for (const [property, values] of Object.entries(shape.values ?? {})) {\n const seen = entry.values.get(property) ?? new Set<string>();\n for (const value of values) seen.add(value);\n entry.values.set(property, seen);\n }\n accumulated.set(boundary.component, entry);\n }\n\n return [...accumulated.entries()]\n .map(([component, entry]) => ({\n component,\n instances: entry.structure.length,\n structure: digestValue(entry.structure),\n semantics: digestValue(entry.semantics),\n text: digestValue(entry.text),\n style: digestValue(entry.style),\n ...(layout ? { geometry: digestValue(entry.geometry) } : {}),\n // Under a profile with no layout every entry is `null`, and a list of\n // nulls is a measurement nobody took written as one they did. Absent\n // instead, which is the same rule `geometry` above obeys.\n ...(layout ? { boxes: entry.boxes } : {}),\n values: sortedValues(entry.values),\n }))\n // Code-unit order, not `localeCompare`. The doc above promises byte-identical\n // output for an unchanged subject, and a locale-aware comparison makes that a\n // promise about the machine's `LANG` — which was tolerable while these were\n // internal and is not now that they are written into a sidecar, committed\n // beside a baseline, and read back on someone else's runner.\n .sort((a, b) => byCodeUnit(a.component, b.component));\n}\n\n/** Properties and their values in code-unit order, so the sidecar is byte-stable. */\nfunction sortedValues(values: ReadonlyMap<string, ReadonlySet<string>>): Record<string, string[]> {\n const sorted: Record<string, string[]> = {};\n for (const property of [...values.keys()].sort(byCodeUnit)) {\n sorted[property] = [...values.get(property)!].sort(byCodeUnit);\n }\n return sorted;\n}\n\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/**\n * Which components *caused* a change, given both revisions' hashes.\n *\n * The arithmetic behind cause-first ranking, and the reason a baseline carries\n * its component hashes at all. `rankRegions` takes a list of cause names and,\n * given none, falls back to area — which\n * [journal 0013](https://github.com/Variance-Authority/variance-authority/blob/main/docs/context/journal/0013-observability.md)\n * measured as backwards by 6×, because area measures displacement rather than\n * cause. This is where that list comes from on a path with no second document.\n *\n * The distinction is the split ADR-0018 built the bands for:\n *\n * - **`structure` or `style` moved** — the component's own content is different.\n * It is a cause.\n * - **only `geometry` moved** — the component is byte-identical and its box is\n * somewhere else. Something *else* moved it, so it is collateral, and naming\n * it would send a reviewer to a file nobody edited.\n * - **appeared or disappeared** — a component present on one side only is a\n * cause. Something decided to render it or to stop.\n *\n * `instances` is deliberately not consulted. A component rendered five times\n * instead of four has a different `structure` digest for the subject, and the\n * count moving on its own — the same component, the same content, one more of\n * them — is a change in whatever decided how many, not in this component.\n *\n * A profile with no layout supplies no `geometry`, so on that tier every\n * difference is `structure` or `style` and every changed component is a cause.\n * That is correct rather than degraded: with no boxes, nothing was displaced.\n */\nexport function causesBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly string[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const causes: string[] = [];\n\n for (const entry of after) {\n // `(unattributed)` is not a component and can never be a cause. It is the\n // bucket for nodes whose provenance chain broke, so it collects unrelated\n // parts of a page under one name — and nothing downstream could act on it\n // anyway: a region with no owner reports no component, so it would never\n // match. A broken chain is a defect in this tool and is reported as\n // `unattributed` where that means something, not smuggled in here as a\n // culprit.\n if (entry.component === UNATTRIBUTED) continue;\n\n const was = previous.get(entry.component);\n if (was === undefined) {\n causes.push(entry.component);\n continue;\n }\n if (ownContentMoved(was, entry)) causes.push(entry.component);\n }\n\n // Removals too, and they are the case a candidate-only walk cannot see: a\n // component that stopped rendering leaves regions behind it, and the component\n // that used to be there is exactly the name a reviewer needs.\n const present = new Set(after.map((entry) => entry.component));\n for (const entry of before) {\n if (entry.component === UNATTRIBUTED) continue;\n if (!present.has(entry.component)) causes.push(entry.component);\n }\n\n return causes.sort();\n}\n\n/**\n * Whether a component's *own content* differs, ignoring where its box ended up.\n *\n * Deliberately not phrased in bands, because it is not a band question. Both\n * `structure` and `geometry` map to the `geometry` band, and this has to keep\n * them apart: a component whose tree changed edited itself, and a component\n * whose rect moved was pushed. That distinction is the entire cause/collateral\n * result, and asking it through the band mapping would need the digests back\n * again to answer it.\n *\n * The list is every digest a component owns except `geometry`. It grew by two on\n * 2026-08-06 without changing meaning: `semantics` and `text` used to be inside\n * `structure`.\n */\nfunction ownContentMoved(before: BandDigests, after: BandDigests): boolean {\n return (\n before.structure !== after.structure ||\n before.semantics !== after.semantics ||\n before.text !== after.text ||\n before.style !== after.style\n );\n}\n\n/**\n * Which frequency bands moved between one component's two hashes.\n *\n * The reason the digests were split. A baseline carries hashes and not\n * documents, so \"what changed here\" used to be answerable only as a boolean —\n * and a boolean cannot serve a route-level test, whose entire request is *tell\n * me when the page stops assembling and never when it is repainted*.\n *\n * The mapping is the same one `bandOf` applies to a delta, but for the one case\n * below, and that is the property that matters: a subject relaxed to `layout` must absorb the\n * same things whether the run held two documents or two sidecars. Two mappings\n * would be one drift away from a config key meaning different things on the two\n * paths, discovered as a regression somebody let through.\n *\n * One delta the digests cannot band as `bandOf` does: the style of an element\n * its author replaced, which `bandOf` bands `geometry` with the tag change. Two\n * hashes do not say the tag changed under the style, so the `style` digest still\n * reads `token` beside it. The `structure` digest moves with the tag, so both\n * paths report `geometry`, and the one level that asserts `token`, `strict`,\n * asserts `geometry` too: the verdict agrees. What differs is the list a\n * relaxed reason gives as absorbed, which names `token` on this path only.\n *\n * | digest | band | what it covers |\n * |---|---|---|\n * | `semantics` | `a11y` | role, accessible name, ARIA state |\n * | `text` | `content` | text runs |\n * | `structure` | `geometry` | tags, aliases, attributes, child boundaries |\n * | `geometry` | `geometry` | rects and computed layout output |\n * | `style` | `token` | declared values and custom properties |\n *\n * `texture` never appears. It is raster residue by definition, and a component\n * hash is built from a document — so the band a comparison of hashes cannot\n * decide is *absent* from the answer rather than reported as unmoved, which is\n * ADR-0002's rule applied to a narrower question.\n *\n * A missing `geometry` on either side is the profile saying it has no layout\n * engine, and is not a difference. Treating absent as a change would report\n * every component as having moved the moment a jsdom baseline met a Chromium\n * run — which the environment key already names as the cause of that\n * comparison, so this would be a second, wronger answer to a question already\n * settled.\n */\nexport function movedBands(before: BandDigests, after: BandDigests): readonly Band[] {\n const moved = new Set<Band>();\n\n if (before.semantics !== after.semantics) moved.add('a11y');\n if (before.text !== after.text) moved.add('content');\n if (before.structure !== after.structure) moved.add('geometry');\n if (before.style !== after.style) moved.add('token');\n if (\n before.geometry !== undefined &&\n after.geometry !== undefined &&\n before.geometry !== after.geometry\n ) {\n moved.add('geometry');\n }\n\n return BANDS.filter((band) => moved.has(band));\n}\n\n/**\n * Every band that moved anywhere in the subject, given both revisions' hashes.\n *\n * A component present on one side only contributes `geometry`: something was\n * added or removed, which is the structural half of that band however the rest\n * of it compares. It deliberately does not contribute `a11y` or `content` as\n * well — a component that is simply not there did not *rename* anything, and\n * inflating the answer would make a level that absorbs nothing look like the\n * only safe choice.\n */\nexport function bandsBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly Band[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const present = new Set(after.map((entry) => entry.component));\n const moved = new Set<Band>();\n\n for (const entry of after) {\n const was = previous.get(entry.component);\n if (was === undefined) moved.add('geometry');\n else for (const band of movedBands(was, entry)) moved.add(band);\n }\n\n for (const entry of before) {\n if (!present.has(entry.component)) moved.add('geometry');\n }\n\n return BANDS.filter((band) => moved.has(band));\n}\n\n/**\n * Which bands moved, kept per component instead of folded into one list.\n *\n * {@link bandsBetween} answers *what kind of change is in this subject*, which is\n * the question a sensitivity level asks. It cannot answer the one a reviewer\n * asks — *what changed, and where* — because the fold is lossy in exactly the\n * place attribution lives: a subject reporting `content, geometry, token` has\n * told you a colour and a string and a size all moved somewhere in it, and left\n * you to guess which of the forty components on the page owns which.\n *\n * Unfolded, the same two sidecars say `Button — geometry, token` and\n * `CardFooter — a11y, content`, and that is a sentence a page can print beside a\n * picture. It is also the record that survives when the raster tier loses the\n * name: a difference that reflowed its neighbours merges into one blob whose box\n * fits no component, so the region resolves to the document root and the edit\n * arrives unattributed — while the hashes, which never looked at a pixel, still\n * hold the component that moved and the sense in which it moved.\n *\n * `cause` repeats the {@link causesBetween} predicate rather than being derived\n * from `bands`, and the repetition is the point: `structure` and `geometry` both\n * map to the `geometry` band, so a component that edited its own tree and one\n * that was merely pushed by a neighbour are indistinguishable *after* the band\n * mapping. Losing that here would make every reflowed container a culprit.\n */\nexport interface ComponentBands {\n readonly component: string;\n /** Non-empty: a component whose digests all matched has no entry at all. */\n readonly bands: readonly Band[];\n /** Its own content moved, as opposed to only its rect. `causesBetween`'s test. */\n readonly cause: boolean;\n /** Set only when the component is on one side alone, which `bands` cannot say. */\n readonly presence?: 'added' | 'removed';\n\n /**\n * How much bigger its own box got, when every instance agrees on the answer.\n *\n * The band a reviewer actually wanted. `geometry` says a rect under here is\n * not the rect it was; this says the control is eight pixels taller and\n * thirty-six wider, which is the padding somebody edited, arriving as a\n * measurement rather than as a guess about which property produced it.\n *\n * Absent for four different reasons and they are one reason: nobody can say.\n * No boxes on one side, a different number of instances, instances that\n * disagree about the delta, or a box that did not change size. Present is\n * always non-zero on at least one axis.\n */\n readonly grew?: { readonly width: number; readonly height: number };\n\n /**\n * Which declared values changed, by property, when both sides recorded them.\n *\n * `token` says a declaration under here moved; this names it. `from` holds the\n * values only the baseline had and `to` the values only this run has, so a\n * component rendered as primary and secondary that changed one background\n * reports that background and not the other. An empty side is a property no\n * node declared on that side, which was measured and is not the same as absent.\n *\n * Absent when either sidecar predates the record, and when the style digest\n * moved but no property's set of values did — a value that moved from one\n * node to another is a real change this cannot describe, and an empty list\n * would say there was none. Present is never empty.\n */\n readonly changed?: readonly ValueChange[];\n}\n\n/** One property whose set of declared values differs between two revisions. */\nexport interface ValueChange {\n readonly property: string;\n readonly from: readonly string[];\n readonly to: readonly string[];\n}\n\n/**\n * Every component whose hashes differ, with the bands it differs in.\n *\n * One-sided components contribute `geometry` and nothing else, which is\n * {@link bandsBetween}'s rule and must stay identical to it: a component that is\n * simply not there did not rename anything, and inflating the answer would put\n * an `a11y` claim on a page for a component nobody can look at. `presence` is\n * what carries the rest of that meaning, so no reader has to infer *appeared*\n * from a lone `geometry`.\n *\n * `(unattributed)` is excluded for the reason {@link causesBetween} excludes it:\n * it is a bucket for nodes whose provenance chain broke, so it collects\n * unrelated parts of a page under one name and nothing downstream could act on\n * it. The broken chain is reported where it means something, not here as a\n * component that moved.\n */\nexport function movedBandsBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly ComponentBands[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const present = new Set(after.map((entry) => entry.component));\n const moved: ComponentBands[] = [];\n\n for (const entry of after) {\n if (entry.component === UNATTRIBUTED) continue;\n const was = previous.get(entry.component);\n if (was === undefined) {\n moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'added' });\n continue;\n }\n const bands = movedBands(was, entry);\n if (bands.length === 0) continue;\n const grew = grewBetween(was.boxes, entry.boxes);\n const changed = was.style === entry.style ? undefined : changedBetween(was.values, entry.values);\n moved.push({\n component: entry.component,\n bands,\n cause: ownContentMoved(was, entry),\n ...(grew === undefined ? {} : { grew }),\n ...(changed === undefined ? {} : { changed }),\n });\n }\n\n for (const entry of before) {\n if (entry.component === UNATTRIBUTED) continue;\n if (present.has(entry.component)) continue;\n moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'removed' });\n }\n\n return moved.sort((left, right) => byCodeUnit(left.component, right.component));\n}\n\n/**\n * Every property whose values differ, or nothing when that cannot be said.\n *\n * Read only when the `style` digest moved, which is the digest these values\n * were declared into: a component whose declarations hashed the same has\n * nothing here to name, whatever else about it changed.\n */\nfunction changedBetween(\n before: ComponentHash['values'],\n after: ComponentHash['values'],\n): readonly ValueChange[] | undefined {\n if (before === undefined || after === undefined) return undefined;\n\n const changed: ValueChange[] = [];\n const properties = new Set([...Object.keys(before), ...Object.keys(after)]);\n for (const property of [...properties].sort(byCodeUnit)) {\n const was = before[property] ?? [];\n const now = after[property] ?? [];\n const from = was.filter((value) => !now.includes(value));\n const to = now.filter((value) => !was.includes(value));\n if (from.length > 0 || to.length > 0) changed.push({ property, from, to });\n }\n return changed.length === 0 ? undefined : changed;\n}\n\n/**\n * One size delta both sides agree on, or nothing.\n *\n * Instances are paired by document order, which is the only order either side\n * has. That pairing is sound exactly while the counts match: a component that\n * gained an instance shifted every index after the insertion, and the deltas\n * that fell out would be measurements of one instance against a different one.\n * So a changed count answers nothing rather than answering wrongly — the count\n * itself already moved `structure`, and the reviewer is told that instead.\n *\n * Instances that disagree also answer nothing. Three buttons where one grew and\n * two did not is a real finding and it is not *this* one, and printing the first\n * or the largest would be the page picking a representative and not saying so.\n *\n * Position is deliberately not read. Everything below a control that got taller\n * moved down, and a delta drawn from `x`/`y` would name every one of them.\n */\nfunction grewBetween(\n before: readonly (Rect | null)[] | undefined,\n after: readonly (Rect | null)[] | undefined,\n): { readonly width: number; readonly height: number } | undefined {\n if (before === undefined || after === undefined) return undefined;\n if (before.length === 0 || before.length !== after.length) return undefined;\n\n let agreed: { width: number; height: number } | undefined;\n for (const [index, was] of before.entries()) {\n const now = after[index];\n if (was === null || was === undefined || now === null || now === undefined) return undefined;\n\n const width = now.width - was.width;\n const height = now.height - was.height;\n if (agreed === undefined) agreed = { width, height };\n else if (agreed.width !== width || agreed.height !== height) return undefined;\n }\n\n return agreed === undefined || (agreed.width === 0 && agreed.height === 0) ? undefined : agreed;\n}\n"]}
@@ -42,7 +42,7 @@ import type { Composition, Site } from './composition.js';
42
42
  * Unexplained is not a flake. This system's position on what establishes a flake
43
43
  * has not moved: a subject is unstable when it fails to read the same way twice,
44
44
  * and one reading can never prove instability
45
- * ([`flakiness.md`](../../../../docs/flakiness.md)). What an unexplained movement
45
+ * ([`flakiness.md`](https://variance-authority.dev/docs/flakiness)). What an unexplained movement
46
46
  * is, exactly, is **the shortlist of subjects worth reading twice** — which is
47
47
  * the scarce resource a sweep spends, and until now it was spent on `changed`
48
48
  * subjects in whatever order the plan produced them.
@@ -1 +1 @@
1
- {"version":3,"file":"movement.js","sourceRoot":"","sources":["../../src/attribute/movement.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Band } from '../compare/band.js';\nimport type { Composition, ComponentEntry, Site } from './composition.js';\nimport { becauseUpstream, unexplainedBecause } from './because.js';\nimport { heldSites } from './control.js';\n\n/**\n * Why a component moved — and what it means when nothing here can say.\n *\n * A run already knows *that* a component's own content differs from its\n * baseline: that is `causesBetween`, and it is where every report starts. What it\n * has never known is whether anybody edited the thing. Those are different\n * questions, and the gap between them is where flakes live:\n *\n * > Detect a pixel change. Find the HTML area behind it. Find no related change.\n * > That is a flake — or the beginning of one.\n *\n * The last step is the one that needs evidence, and the evidence has to be\n * assembled rather than assumed. Four kinds are available to a run that has\n * already done its work, in descending order of how much they explain:\n *\n * 1. **An edit.** A file declaring this component is in the change set. Nothing\n * further is interesting; somebody changed it and the run noticed.\n * 2. **A token.** A custom property this component's own nodes resolve through\n * took a new value in this run. The component's code is untouched and its\n * output legitimately moved.\n * 3. **An ancestor.** A component that reaches it was edited, and it is reached\n * by climbing — the caller that wrote the element, or, on a build where React\n * kept no owner, the enclosure graph of the subject it moved in until an\n * edited name appears. Its props are not recoverable in detail, but *this\n * moved on what it was given* is the sentence, and it names a real file.\n * 4. **A contradiction.** The same component, with the same props, rendered\n * somewhere else in this same run and rendered *differently*. That is not an\n * explanation — it is proof the explanation is not in the component's own\n * code, because one commit produced two answers from one input.\n *\n * And then **unexplained**, which is the finding. A component whose content\n * moved, whose file nobody touched, whose tokens held, whose ancestors held, and\n * which rendered identically everywhere else in the suite at this commit. That\n * last clause is the part no previous version of this could say, and it is what\n * the cross-subject join bought: the *stable states to refer to* are the other\n * sites of the same rendering, and the run already has them.\n *\n * ## What this refuses to conclude\n *\n * Unexplained is not a flake. This system's position on what establishes a flake\n * has not moved: a subject is unstable when it fails to read the same way twice,\n * and one reading can never prove instability\n * ([`flakiness.md`](../../../../docs/flakiness.md)). What an unexplained movement\n * is, exactly, is **the shortlist of subjects worth reading twice** — which is\n * the scarce resource a sweep spends, and until now it was spent on `changed`\n * subjects in whatever order the plan produced them.\n */\n\nexport type Cause = 'edited' | 'token' | 'upstream' | 'contradicted' | 'unexplained';\n\n/** One component the run found to have moved in one subject. */\nexport interface Moved {\n readonly subject: string;\n readonly component: string;\n /**\n * Which bands moved, when the run could tell.\n *\n * Empty when the comparison produced names only — a baseline written before\n * the bands were split carries no per-band digests, and an empty list here\n * means *not known*, never *no band*.\n */\n readonly bands: readonly Band[];\n}\n\n/** What a run can put beside a movement to explain it. */\nexport interface Evidence {\n /**\n * Files in the change set, from whichever flag read a diff. Absent when none did.\n *\n * Absent and empty are different answers and are kept different: a run with no\n * `--since` has not established that nobody edited anything, and reading its\n * silence as \"no edits\" would attribute every movement in the suite to nothing\n * and call the result a flake list.\n */\n readonly changed?: readonly string[];\n\n /** Component name → the files that declare it, from the source index. */\n readonly declaredIn?: ReadonlyMap<string, readonly string[]>;\n\n /** Tokens whose resolved value moved in this run. Absent when nothing asked. */\n readonly tokens?: readonly string[];\n\n /** Subjects that failed to read the same way twice in this run. */\n readonly unstable?: ReadonlySet<string>;\n\n /**\n * Every component whose hashes moved, by subject — the whole reading, not the\n * causes.\n *\n * The control group is read from this and not from the movements, because the\n * movements are a *filtered* set: they come from regions the pixels named as\n * causes, so a component whose hashes moved and whose box no region named is\n * absent from them. Filtering the control group by the movements alone hands\n * back exactly those renders as *held still*, which is the one claim that\n * makes a control group worse than no control group.\n *\n * **Absent is not empty**, twice over. An absent map is a run that never\n * compared hashes, and the control group falls back to the movements with a\n * weaker guarantee. A subject *missing from* a present map had no hashes read\n * there — nobody measured it, so it cannot be a control for anything.\n */\n readonly hashesMoved?: ReadonlyMap<string, ReadonlySet<string>>;\n}\n\nexport interface Movement {\n readonly subject: string;\n readonly component: string;\n readonly bands: readonly Band[];\n readonly cause: Cause;\n /** One sentence, naming the evidence rather than the category. */\n readonly because: string;\n\n readonly file?: string;\n readonly tokens?: readonly string[];\n readonly upstream?: string;\n\n /**\n * Components between `upstream` and this one, outermost first.\n *\n * Empty when the edited component draws it directly, and present only on the\n * `upstream` rung. A reviewer sent to `ProductCard` for a `CardFooter` that\n * moved has one question — *how does that reach this* — and the answer is two\n * names the run already holds.\n */\n readonly through?: readonly string[];\n\n /**\n * Other subjects in which this same component moved in this run.\n *\n * The \"one cause, N subjects\" fold. A reviewer reading eleven changed subjects\n * is often reading one edit, and the count is the difference between a\n * frightening report and an accurate one.\n */\n readonly alsoIn: readonly string[];\n\n /**\n * Sites of this component, with the same props, whose hashes this run read and\n * found unmoved.\n *\n * The control group, and the reason an unexplained movement is worth\n * reporting rather than shrugging at. Empty means there was no control — the\n * component appears nowhere else with these inputs, or it moved in every one\n * of them — which weakens the finding and is why it is a list rather than a\n * flag.\n */\n readonly held: readonly Site[];\n\n /**\n * Sites of this component with the same props, other than this one, that the\n * run read — the pool `held` was drawn from.\n *\n * The denominator, and without it `held: []` is two opposite findings under one\n * shape. Zero is *nothing to compare against*; four with an empty `held` is a\n * comparison that was made, in four renders, and came back the same way in all\n * of them — the statement that nothing rendered under those props escaped\n * whatever moved here.\n */\n readonly compared: number;\n}\n\nexport interface Attribution {\n /** Every movement supplied, in the order supplied, each with a cause. */\n readonly movements: readonly Movement[];\n\n /**\n * Unexplained movements in subjects that also failed to read the same way\n * twice. A movement nothing explains, in a subject already proven unstable.\n */\n readonly flakes: readonly Movement[];\n\n /**\n * Unexplained movements in subjects nothing has read twice.\n *\n * Not a verdict. The shortlist a sweep should spend its second readings on,\n * ordered by how much control the suite has over each — a component that\n * rendered identically in six other places and moved here is a better use of a\n * second reading than one that appears nowhere else.\n */\n readonly suspects: readonly Movement[];\n}\n\n/**\n * Attribute every movement, given whatever evidence the run assembled.\n *\n * Degrades honestly. With no change set the `edited` rung is unreachable and\n * every movement falls to a lower one, so a run that did not ask cannot produce\n * a confident `unexplained` — which is checked here rather than trusted to the\n * caller, because the shortlist this feeds is exactly the thing somebody will\n * act on.\n */\nexport function attributeMovement(\n moved: readonly Moved[],\n composition: Composition,\n evidence: Evidence = {},\n): Attribution {\n const byComponent = new Map(composition.components.map((entry) => [entry.component, entry]));\n const movedIn = new Map<string, string[]>();\n for (const each of moved) {\n const subjects = movedIn.get(each.component) ?? [];\n subjects.push(each.subject);\n movedIn.set(each.component, subjects);\n }\n\n const edited = new Set(\n [...(evidence.declaredIn ?? new Map<string, readonly string[]>())]\n .filter(([, files]) => files.some((file) => (evidence.changed ?? []).includes(file)))\n .map(([component]) => component),\n );\n\n const graph = graphOf(composition);\n\n const movements = moved.map((each) =>\n attributeOne(each, byComponent.get(each.component), {\n movedIn,\n edited,\n evidence,\n composition,\n graph,\n }),\n );\n\n const unexplained = movements.filter((movement) => movement.cause === 'unexplained');\n\n return {\n movements,\n flakes: unexplained.filter((movement) => evidence.unstable?.has(movement.subject) === true),\n suspects: unexplained\n .filter((movement) => evidence.unstable?.has(movement.subject) !== true)\n .sort((a, b) => b.held.length - a.held.length),\n };\n}\n\n/** Everything the run assembled, gathered so one movement can be asked about. */\nexport interface Bench {\n readonly movedIn: ReadonlyMap<string, readonly string[]>;\n readonly edited: ReadonlySet<string>;\n readonly evidence: Evidence;\n readonly composition: Composition;\n readonly graph: Graph;\n}\n\nfunction attributeOne(moved: Moved, entry: ComponentEntry | undefined, bench: Bench): Movement {\n const { movedIn, edited, evidence, composition, graph } = bench;\n const alsoIn = (movedIn.get(moved.component) ?? []).filter(\n (subject) => subject !== moved.subject,\n );\n const { held, considered } = heldSites(moved, entry, bench);\n const base = { ...moved, alsoIn, held, compared: considered };\n\n const file = (evidence.declaredIn?.get(moved.component) ?? []).find((each) =>\n (evidence.changed ?? []).includes(each),\n );\n if (file !== undefined) {\n return { ...base, cause: 'edited', file, because: `\\`${file}\\` is in the change set` };\n }\n\n const tokens = movedTokens(moved.component, composition, evidence);\n if (tokens.length > 0) {\n return {\n ...base,\n cause: 'token',\n tokens,\n because: `it resolves through ${tokens.map((token) => `\\`${token}\\``).join(', ')}, which moved in this run`,\n };\n }\n\n const ancestor = editedAncestor(moved, edited, graph);\n if (ancestor !== undefined) {\n return {\n ...base,\n cause: 'upstream',\n upstream: ancestor.name,\n ...(ancestor.through.length === 0 ? {} : { through: ancestor.through }),\n because: becauseUpstream(ancestor),\n };\n }\n\n const contradiction = composition.divergences.find(\n (divergence) =>\n divergence.component === moved.component &&\n divergence.renderings.some((rendering) =>\n rendering.sites.some((site) => site.subject === moved.subject),\n ),\n );\n if (contradiction !== undefined) {\n return {\n ...base,\n cause: 'contradicted',\n because:\n `at this commit it renders ${contradiction.renderings.length} different ways from the same props ` +\n `(${contradiction.bands.join(', ')}), so the change is not in its own code`,\n };\n }\n\n return { ...base, cause: 'unexplained', because: unexplainedBecause(base, considered, evidence) };\n}\n\n/**\n * Who encloses what and who wrote what, in each subject separately.\n *\n * Keyed on the pair because the constraint is the whole point. `ComponentEntry.within`\n * and `ComponentEntry.createdBy` are both folded over the suite, so `Card` is enclosed by\n * `CartCard` and by `ProductCard` at once, and a walk that reads either hands a\n * reviewer the cart as the reason a product story moved. A site knows which\n * subject it was read in, so the graph can be built the way the question is\n * asked — and it has to be built that way for *both* edges, because the creator\n * is consulted first and an unconstrained answer there is never reached by the\n * constrained walk underneath it.\n */\ntype Enclosures = ReadonlyMap<string, readonly string[]>;\n\ninterface Graph {\n /** Who this component sits inside, per subject. */\n readonly within: Enclosures;\n /** Whose JSX wrote this component's element, per subject. */\n readonly wrote: Enclosures;\n}\n\nconst IN = '\\u0000';\n\nfunction graphOf(composition: Composition): Graph {\n const within = new Map<string, string[]>();\n const wrote = new Map<string, string[]>();\n\n const add = (graph: Map<string, string[]>, key: string, name: string): void => {\n const names = graph.get(key);\n if (names === undefined) graph.set(key, [name]);\n else if (!names.includes(name)) names.push(name);\n };\n\n for (const entry of composition.components) {\n for (const group of entry.classes) {\n for (const rendering of group.renderings) {\n for (const site of rendering.sites) {\n const key = `${site.subject}${IN}${entry.component}`;\n if (site.within !== undefined) add(within, key, site.within);\n if (site.createdBy !== undefined) add(wrote, key, site.createdBy);\n }\n }\n }\n }\n\n for (const names of within.values()) names.sort();\n for (const names of wrote.values()) names.sort();\n return { within, wrote };\n}\n\n/** An edited component above a moved one, and the components in between. */\nexport interface Ancestor {\n readonly name: string;\n /** Outermost first, and empty when the edited component draws it directly. */\n readonly through: readonly string[];\n}\n\n/**\n * The nearest edited component above this one, in the subject it moved in.\n *\n * The creator first, and it is not a tie-break. The component that *wrote the\n * element* is the one whose edit changed this component's inputs; the one it\n * happens to sit inside may be a presentational wrapper that knows nothing about\n * it. On `examples/todomvc` every `Chip` sits within a `Stack` and is created by\n * `TodoFooter`, so a run consulting only enclosure would fail to connect an edit\n * to `TodoFooter` with the chips it moved — and report five unexplained movements\n * instead of one caller. It is absent on a production build, which is why the\n * enclosure walk is not a fallback but the other half.\n *\n * Read off the per-subject graph and not off the entry. The entry's `createdBy`\n * is the suite's whole set, so an edit to whoever writes this component on one\n * page answers for every page it appears on — the same unconstrained answer the\n * walk below refuses, arriving one line earlier and winning.\n *\n * And that walk climbs rather than looking once. The rung has always said\n * *ancestor* and checked a parent, and a React tree is mostly components that\n * draw one wrapper each: `ProductCard` was edited, `ProductCard` draws `Card`,\n * `Card` draws `CardFooter`, and one look up arrives at `Card`, whose file nobody\n * touched. That is the shape of every unexplained movement this ladder used to\n * produce over an edit it was holding the graph for.\n */\nfunction editedAncestor(\n moved: Moved,\n edited: ReadonlySet<string>,\n graph: Graph,\n): Ancestor | undefined {\n const here = `${moved.subject}${IN}${moved.component}`;\n const wrote = (graph.wrote.get(here) ?? []).find((name) => edited.has(name));\n if (wrote !== undefined) return { name: wrote, through: [] };\n\n // Breadth-first, so the answer is the *nearest* edit and not whichever one the\n // recursion reached first. `seen` is written as a rung is built: an enclosure\n // graph folded over a suite can close a loop, and a component reached twice on\n // one rung would otherwise queue twice.\n const seen = new Set([moved.component]);\n const step = (from: Ancestor | null): Ancestor[] => {\n const child = from === null ? moved.component : from.name;\n const rung: Ancestor[] = [];\n for (const holder of graph.within.get(`${moved.subject}${IN}${child}`) ?? []) {\n if (seen.has(holder)) continue;\n seen.add(holder);\n rung.push({ name: holder, through: from === null ? [] : [from.name, ...from.through] });\n }\n return rung;\n };\n\n let rung = step(null);\n while (rung.length > 0) {\n const arrived = rung.find((each) => edited.has(each.name));\n if (arrived !== undefined) return arrived;\n rung = rung.flatMap(step);\n }\n\n return undefined;\n}\n\n/**\n * Tokens this component resolves through that moved in this run.\n *\n * Read off the instances rather than off the subject, which is the distinction\n * that makes the answer worth anything: every subject on a themed page resolves\n * through every token in the theme, so a subject-level intersection names them\n * all and explains nothing.\n */\nfunction movedTokens(\n component: string,\n composition: Composition,\n evidence: Evidence,\n): readonly string[] {\n if (evidence.tokens === undefined || evidence.tokens.length === 0) return [];\n\n const entry = composition.components.find((each) => each.component === component);\n if (entry === undefined) return [];\n\n const moved = new Set(evidence.tokens);\n return [...entry.tokens].filter((token) => moved.has(token)).sort();\n}\n"]}
1
+ {"version":3,"file":"movement.js","sourceRoot":"","sources":["../../src/attribute/movement.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Band } from '../compare/band.js';\nimport type { Composition, ComponentEntry, Site } from './composition.js';\nimport { becauseUpstream, unexplainedBecause } from './because.js';\nimport { heldSites } from './control.js';\n\n/**\n * Why a component moved — and what it means when nothing here can say.\n *\n * A run already knows *that* a component's own content differs from its\n * baseline: that is `causesBetween`, and it is where every report starts. What it\n * has never known is whether anybody edited the thing. Those are different\n * questions, and the gap between them is where flakes live:\n *\n * > Detect a pixel change. Find the HTML area behind it. Find no related change.\n * > That is a flake — or the beginning of one.\n *\n * The last step is the one that needs evidence, and the evidence has to be\n * assembled rather than assumed. Four kinds are available to a run that has\n * already done its work, in descending order of how much they explain:\n *\n * 1. **An edit.** A file declaring this component is in the change set. Nothing\n * further is interesting; somebody changed it and the run noticed.\n * 2. **A token.** A custom property this component's own nodes resolve through\n * took a new value in this run. The component's code is untouched and its\n * output legitimately moved.\n * 3. **An ancestor.** A component that reaches it was edited, and it is reached\n * by climbing — the caller that wrote the element, or, on a build where React\n * kept no owner, the enclosure graph of the subject it moved in until an\n * edited name appears. Its props are not recoverable in detail, but *this\n * moved on what it was given* is the sentence, and it names a real file.\n * 4. **A contradiction.** The same component, with the same props, rendered\n * somewhere else in this same run and rendered *differently*. That is not an\n * explanation — it is proof the explanation is not in the component's own\n * code, because one commit produced two answers from one input.\n *\n * And then **unexplained**, which is the finding. A component whose content\n * moved, whose file nobody touched, whose tokens held, whose ancestors held, and\n * which rendered identically everywhere else in the suite at this commit. That\n * last clause is the part no previous version of this could say, and it is what\n * the cross-subject join bought: the *stable states to refer to* are the other\n * sites of the same rendering, and the run already has them.\n *\n * ## What this refuses to conclude\n *\n * Unexplained is not a flake. This system's position on what establishes a flake\n * has not moved: a subject is unstable when it fails to read the same way twice,\n * and one reading can never prove instability\n * ([`flakiness.md`](https://variance-authority.dev/docs/flakiness)). What an unexplained movement\n * is, exactly, is **the shortlist of subjects worth reading twice** — which is\n * the scarce resource a sweep spends, and until now it was spent on `changed`\n * subjects in whatever order the plan produced them.\n */\n\nexport type Cause = 'edited' | 'token' | 'upstream' | 'contradicted' | 'unexplained';\n\n/** One component the run found to have moved in one subject. */\nexport interface Moved {\n readonly subject: string;\n readonly component: string;\n /**\n * Which bands moved, when the run could tell.\n *\n * Empty when the comparison produced names only — a baseline written before\n * the bands were split carries no per-band digests, and an empty list here\n * means *not known*, never *no band*.\n */\n readonly bands: readonly Band[];\n}\n\n/** What a run can put beside a movement to explain it. */\nexport interface Evidence {\n /**\n * Files in the change set, from whichever flag read a diff. Absent when none did.\n *\n * Absent and empty are different answers and are kept different: a run with no\n * `--since` has not established that nobody edited anything, and reading its\n * silence as \"no edits\" would attribute every movement in the suite to nothing\n * and call the result a flake list.\n */\n readonly changed?: readonly string[];\n\n /** Component name → the files that declare it, from the source index. */\n readonly declaredIn?: ReadonlyMap<string, readonly string[]>;\n\n /** Tokens whose resolved value moved in this run. Absent when nothing asked. */\n readonly tokens?: readonly string[];\n\n /** Subjects that failed to read the same way twice in this run. */\n readonly unstable?: ReadonlySet<string>;\n\n /**\n * Every component whose hashes moved, by subject — the whole reading, not the\n * causes.\n *\n * The control group is read from this and not from the movements, because the\n * movements are a *filtered* set: they come from regions the pixels named as\n * causes, so a component whose hashes moved and whose box no region named is\n * absent from them. Filtering the control group by the movements alone hands\n * back exactly those renders as *held still*, which is the one claim that\n * makes a control group worse than no control group.\n *\n * **Absent is not empty**, twice over. An absent map is a run that never\n * compared hashes, and the control group falls back to the movements with a\n * weaker guarantee. A subject *missing from* a present map had no hashes read\n * there — nobody measured it, so it cannot be a control for anything.\n */\n readonly hashesMoved?: ReadonlyMap<string, ReadonlySet<string>>;\n}\n\nexport interface Movement {\n readonly subject: string;\n readonly component: string;\n readonly bands: readonly Band[];\n readonly cause: Cause;\n /** One sentence, naming the evidence rather than the category. */\n readonly because: string;\n\n readonly file?: string;\n readonly tokens?: readonly string[];\n readonly upstream?: string;\n\n /**\n * Components between `upstream` and this one, outermost first.\n *\n * Empty when the edited component draws it directly, and present only on the\n * `upstream` rung. A reviewer sent to `ProductCard` for a `CardFooter` that\n * moved has one question — *how does that reach this* — and the answer is two\n * names the run already holds.\n */\n readonly through?: readonly string[];\n\n /**\n * Other subjects in which this same component moved in this run.\n *\n * The \"one cause, N subjects\" fold. A reviewer reading eleven changed subjects\n * is often reading one edit, and the count is the difference between a\n * frightening report and an accurate one.\n */\n readonly alsoIn: readonly string[];\n\n /**\n * Sites of this component, with the same props, whose hashes this run read and\n * found unmoved.\n *\n * The control group, and the reason an unexplained movement is worth\n * reporting rather than shrugging at. Empty means there was no control — the\n * component appears nowhere else with these inputs, or it moved in every one\n * of them — which weakens the finding and is why it is a list rather than a\n * flag.\n */\n readonly held: readonly Site[];\n\n /**\n * Sites of this component with the same props, other than this one, that the\n * run read — the pool `held` was drawn from.\n *\n * The denominator, and without it `held: []` is two opposite findings under one\n * shape. Zero is *nothing to compare against*; four with an empty `held` is a\n * comparison that was made, in four renders, and came back the same way in all\n * of them — the statement that nothing rendered under those props escaped\n * whatever moved here.\n */\n readonly compared: number;\n}\n\nexport interface Attribution {\n /** Every movement supplied, in the order supplied, each with a cause. */\n readonly movements: readonly Movement[];\n\n /**\n * Unexplained movements in subjects that also failed to read the same way\n * twice. A movement nothing explains, in a subject already proven unstable.\n */\n readonly flakes: readonly Movement[];\n\n /**\n * Unexplained movements in subjects nothing has read twice.\n *\n * Not a verdict. The shortlist a sweep should spend its second readings on,\n * ordered by how much control the suite has over each — a component that\n * rendered identically in six other places and moved here is a better use of a\n * second reading than one that appears nowhere else.\n */\n readonly suspects: readonly Movement[];\n}\n\n/**\n * Attribute every movement, given whatever evidence the run assembled.\n *\n * Degrades honestly. With no change set the `edited` rung is unreachable and\n * every movement falls to a lower one, so a run that did not ask cannot produce\n * a confident `unexplained` — which is checked here rather than trusted to the\n * caller, because the shortlist this feeds is exactly the thing somebody will\n * act on.\n */\nexport function attributeMovement(\n moved: readonly Moved[],\n composition: Composition,\n evidence: Evidence = {},\n): Attribution {\n const byComponent = new Map(composition.components.map((entry) => [entry.component, entry]));\n const movedIn = new Map<string, string[]>();\n for (const each of moved) {\n const subjects = movedIn.get(each.component) ?? [];\n subjects.push(each.subject);\n movedIn.set(each.component, subjects);\n }\n\n const edited = new Set(\n [...(evidence.declaredIn ?? new Map<string, readonly string[]>())]\n .filter(([, files]) => files.some((file) => (evidence.changed ?? []).includes(file)))\n .map(([component]) => component),\n );\n\n const graph = graphOf(composition);\n\n const movements = moved.map((each) =>\n attributeOne(each, byComponent.get(each.component), {\n movedIn,\n edited,\n evidence,\n composition,\n graph,\n }),\n );\n\n const unexplained = movements.filter((movement) => movement.cause === 'unexplained');\n\n return {\n movements,\n flakes: unexplained.filter((movement) => evidence.unstable?.has(movement.subject) === true),\n suspects: unexplained\n .filter((movement) => evidence.unstable?.has(movement.subject) !== true)\n .sort((a, b) => b.held.length - a.held.length),\n };\n}\n\n/** Everything the run assembled, gathered so one movement can be asked about. */\nexport interface Bench {\n readonly movedIn: ReadonlyMap<string, readonly string[]>;\n readonly edited: ReadonlySet<string>;\n readonly evidence: Evidence;\n readonly composition: Composition;\n readonly graph: Graph;\n}\n\nfunction attributeOne(moved: Moved, entry: ComponentEntry | undefined, bench: Bench): Movement {\n const { movedIn, edited, evidence, composition, graph } = bench;\n const alsoIn = (movedIn.get(moved.component) ?? []).filter(\n (subject) => subject !== moved.subject,\n );\n const { held, considered } = heldSites(moved, entry, bench);\n const base = { ...moved, alsoIn, held, compared: considered };\n\n const file = (evidence.declaredIn?.get(moved.component) ?? []).find((each) =>\n (evidence.changed ?? []).includes(each),\n );\n if (file !== undefined) {\n return { ...base, cause: 'edited', file, because: `\\`${file}\\` is in the change set` };\n }\n\n const tokens = movedTokens(moved.component, composition, evidence);\n if (tokens.length > 0) {\n return {\n ...base,\n cause: 'token',\n tokens,\n because: `it resolves through ${tokens.map((token) => `\\`${token}\\``).join(', ')}, which moved in this run`,\n };\n }\n\n const ancestor = editedAncestor(moved, edited, graph);\n if (ancestor !== undefined) {\n return {\n ...base,\n cause: 'upstream',\n upstream: ancestor.name,\n ...(ancestor.through.length === 0 ? {} : { through: ancestor.through }),\n because: becauseUpstream(ancestor),\n };\n }\n\n const contradiction = composition.divergences.find(\n (divergence) =>\n divergence.component === moved.component &&\n divergence.renderings.some((rendering) =>\n rendering.sites.some((site) => site.subject === moved.subject),\n ),\n );\n if (contradiction !== undefined) {\n return {\n ...base,\n cause: 'contradicted',\n because:\n `at this commit it renders ${contradiction.renderings.length} different ways from the same props ` +\n `(${contradiction.bands.join(', ')}), so the change is not in its own code`,\n };\n }\n\n return { ...base, cause: 'unexplained', because: unexplainedBecause(base, considered, evidence) };\n}\n\n/**\n * Who encloses what and who wrote what, in each subject separately.\n *\n * Keyed on the pair because the constraint is the whole point. `ComponentEntry.within`\n * and `ComponentEntry.createdBy` are both folded over the suite, so `Card` is enclosed by\n * `CartCard` and by `ProductCard` at once, and a walk that reads either hands a\n * reviewer the cart as the reason a product story moved. A site knows which\n * subject it was read in, so the graph can be built the way the question is\n * asked — and it has to be built that way for *both* edges, because the creator\n * is consulted first and an unconstrained answer there is never reached by the\n * constrained walk underneath it.\n */\ntype Enclosures = ReadonlyMap<string, readonly string[]>;\n\ninterface Graph {\n /** Who this component sits inside, per subject. */\n readonly within: Enclosures;\n /** Whose JSX wrote this component's element, per subject. */\n readonly wrote: Enclosures;\n}\n\nconst IN = '\\u0000';\n\nfunction graphOf(composition: Composition): Graph {\n const within = new Map<string, string[]>();\n const wrote = new Map<string, string[]>();\n\n const add = (graph: Map<string, string[]>, key: string, name: string): void => {\n const names = graph.get(key);\n if (names === undefined) graph.set(key, [name]);\n else if (!names.includes(name)) names.push(name);\n };\n\n for (const entry of composition.components) {\n for (const group of entry.classes) {\n for (const rendering of group.renderings) {\n for (const site of rendering.sites) {\n const key = `${site.subject}${IN}${entry.component}`;\n if (site.within !== undefined) add(within, key, site.within);\n if (site.createdBy !== undefined) add(wrote, key, site.createdBy);\n }\n }\n }\n }\n\n for (const names of within.values()) names.sort();\n for (const names of wrote.values()) names.sort();\n return { within, wrote };\n}\n\n/** An edited component above a moved one, and the components in between. */\nexport interface Ancestor {\n readonly name: string;\n /** Outermost first, and empty when the edited component draws it directly. */\n readonly through: readonly string[];\n}\n\n/**\n * The nearest edited component above this one, in the subject it moved in.\n *\n * The creator first, and it is not a tie-break. The component that *wrote the\n * element* is the one whose edit changed this component's inputs; the one it\n * happens to sit inside may be a presentational wrapper that knows nothing about\n * it. On `examples/todomvc` every `Chip` sits within a `Stack` and is created by\n * `TodoFooter`, so a run consulting only enclosure would fail to connect an edit\n * to `TodoFooter` with the chips it moved — and report five unexplained movements\n * instead of one caller. It is absent on a production build, which is why the\n * enclosure walk is not a fallback but the other half.\n *\n * Read off the per-subject graph and not off the entry. The entry's `createdBy`\n * is the suite's whole set, so an edit to whoever writes this component on one\n * page answers for every page it appears on — the same unconstrained answer the\n * walk below refuses, arriving one line earlier and winning.\n *\n * And that walk climbs rather than looking once. The rung has always said\n * *ancestor* and checked a parent, and a React tree is mostly components that\n * draw one wrapper each: `ProductCard` was edited, `ProductCard` draws `Card`,\n * `Card` draws `CardFooter`, and one look up arrives at `Card`, whose file nobody\n * touched. That is the shape of every unexplained movement this ladder used to\n * produce over an edit it was holding the graph for.\n */\nfunction editedAncestor(\n moved: Moved,\n edited: ReadonlySet<string>,\n graph: Graph,\n): Ancestor | undefined {\n const here = `${moved.subject}${IN}${moved.component}`;\n const wrote = (graph.wrote.get(here) ?? []).find((name) => edited.has(name));\n if (wrote !== undefined) return { name: wrote, through: [] };\n\n // Breadth-first, so the answer is the *nearest* edit and not whichever one the\n // recursion reached first. `seen` is written as a rung is built: an enclosure\n // graph folded over a suite can close a loop, and a component reached twice on\n // one rung would otherwise queue twice.\n const seen = new Set([moved.component]);\n const step = (from: Ancestor | null): Ancestor[] => {\n const child = from === null ? moved.component : from.name;\n const rung: Ancestor[] = [];\n for (const holder of graph.within.get(`${moved.subject}${IN}${child}`) ?? []) {\n if (seen.has(holder)) continue;\n seen.add(holder);\n rung.push({ name: holder, through: from === null ? [] : [from.name, ...from.through] });\n }\n return rung;\n };\n\n let rung = step(null);\n while (rung.length > 0) {\n const arrived = rung.find((each) => edited.has(each.name));\n if (arrived !== undefined) return arrived;\n rung = rung.flatMap(step);\n }\n\n return undefined;\n}\n\n/**\n * Tokens this component resolves through that moved in this run.\n *\n * Read off the instances rather than off the subject, which is the distinction\n * that makes the answer worth anything: every subject on a themed page resolves\n * through every token in the theme, so a subject-level intersection names them\n * all and explains nothing.\n */\nfunction movedTokens(\n component: string,\n composition: Composition,\n evidence: Evidence,\n): readonly string[] {\n if (evidence.tokens === undefined || evidence.tokens.length === 0) return [];\n\n const entry = composition.components.find((each) => each.component === component);\n if (entry === undefined) return [];\n\n const moved = new Set(evidence.tokens);\n return [...entry.tokens].filter((token) => moved.has(token)).sort();\n}\n"]}
@@ -135,6 +135,49 @@ export interface AttributionOptions {
135
135
  * is named rather than buried.
136
136
  */
137
137
  readonly containment?: number;
138
+ /**
139
+ * The snapshot of the render the pixels are compared against.
140
+ *
141
+ * Required, and `undefined` is an answer: it says the other render is not
142
+ * known. A changed region is the union of what left and what arrived, so each
143
+ * node is joined against its box in this render and the box of the node the
144
+ * differ's matcher pairs it with in that one. Without it a control replaced
145
+ * by a larger element changes pixels outside either box alone, and the join
146
+ * names the row it sits in.
147
+ *
148
+ * Its boxes are placed by where they were in its own image, whose corner is
149
+ * {@link beforeOrigin}. A shot clipped to the subject starts at its root, so
150
+ * a root the page moved between the two shots moves none of the pixels, and
151
+ * each box that left is shifted by it.
152
+ */
153
+ readonly before: SemanticSnapshot | undefined;
154
+ /**
155
+ * Page-space CSS origin of the before image's top-left corner.
156
+ *
157
+ * Defaults to `before`'s own root rect, as `origin` defaults to this
158
+ * render's. Required when `origin` is stated and `before` is a snapshot: two
159
+ * shots clipped to anything but the root share a corner only by the page's
160
+ * layout, and a corner guessed from the other image gives a plausible report
161
+ * about the wrong components. A full-page pair passes `{x: 0, y: 0}` for both.
162
+ */
163
+ readonly beforeOrigin?: {
164
+ readonly x: number;
165
+ readonly y: number;
166
+ };
167
+ /**
168
+ * The rows of this image that have no partner in the before image, as
169
+ * half-open runs of raster rows.
170
+ *
171
+ * Such a row is marked edge to edge, because nothing says where its content
172
+ * begins, so a region inside one is as wide as the image and only the root
173
+ * contains it by area. It is joined by its rows instead: to the tightest box
174
+ * whose rows span it, which is the box that grew. Absent when the rows were
175
+ * compared by position.
176
+ */
177
+ readonly inserted?: readonly {
178
+ readonly from: number;
179
+ readonly to: number;
180
+ }[];
138
181
  }
139
182
  /**
140
183
  * Join isolated regions to the nodes that occupy them.
@@ -156,13 +199,13 @@ export interface RankedRegion extends AttributedRegion {
156
199
  *
157
200
  * This function exists because of a measurement, and the measurement is the most
158
201
  * useful thing the raster tier has produced. Replacing a checkbox with a styled
159
- * div on the todomvc corpus changes 1530 pixels in 5 regions, which attribute
202
+ * div on the todomvc corpus changes 1853 pixels in 5 regions, which attribute
160
203
  * geometrically and rank by area as:
161
204
  *
162
205
  * ```
163
- * 933px Text <- changed, and reflowed
164
- * 511px Stack <- only reflowed
165
- * 86px Toggle <- the edit
206
+ * 1055px Text <- changed, and reflowed
207
+ * 614px Stack <- only reflowed
208
+ * 184px Toggle <- the edit
166
209
  * ```
167
210
  *
168
211
  * Every one of those attributions is correct. The pixels really are inside those
@@ -1,5 +1,6 @@
1
1
  import { intersectionArea } from '../format/capture.js';
2
2
  import { locate } from './locate.js';
3
+ import { matchTrees } from '../compare/diff/match.js';
3
4
  const DEFAULT_CONTAINMENT = 0.9;
4
5
  /**
5
6
  * Join isolated regions to the nodes that occupy them.
@@ -9,8 +10,19 @@ const DEFAULT_CONTAINMENT = 0.9;
9
10
  * correct answer — a rect that was never observed must never be inferred.
10
11
  */
11
12
  export function attributeRegions(regions, snapshot, options) {
12
- const boxed = collectBoxes(snapshot.root);
13
+ if (options.origin !== undefined && options.before !== undefined && options.beforeOrigin === undefined) {
14
+ throw new Error('`origin` is stated and `beforeOrigin` is not: state the corner of the before image too, ' +
15
+ 'since two shots share one only when the page did not move between them');
16
+ }
13
17
  const origin = options.origin ?? snapshot.root.rect ?? { x: 0, y: 0 };
18
+ // A box that left is placed by where it was in its image, not on the page.
19
+ const was = options.beforeOrigin ?? options.before?.root.rect ?? origin;
20
+ const left = keptBoxes(options.before, snapshot, { x: origin.x - was.x, y: origin.y - was.y });
21
+ const boxed = collectBoxes(snapshot.root).map((node) => {
22
+ const before = left.get(node);
23
+ const rects = before === undefined ? [node.rect] : [node.rect, before];
24
+ return { node, rects, area: extent(rects) };
25
+ });
14
26
  const containment = options.containment ?? DEFAULT_CONTAINMENT;
15
27
  return regions.map((region) => {
16
28
  const css = {
@@ -20,14 +32,16 @@ export function attributeRegions(regions, snapshot, options) {
20
32
  height: region.height / options.scale,
21
33
  };
22
34
  const area = css.width * css.height;
35
+ // Rows the after image added are joined by the rows they span, and only to
36
+ // this render's box: the before side never painted them.
37
+ const added = options.inserted?.some((run) => run.from <= region.y && region.y + region.height <= run.to) ?? false;
23
38
  let inside;
24
39
  let overlapping;
25
- for (const node of boxed) {
26
- const rect = node.rect;
27
- const shared = intersectionArea(css, rect);
40
+ for (const box of boxed) {
41
+ const shared = covered(css, box.rects);
28
42
  if (shared <= 0)
29
43
  continue;
30
- const nodeArea = rect.width * rect.height;
44
+ const nodeArea = box.area;
31
45
  // `<=`, not `<`, and the difference is one component name in every report
32
46
  // over a design system. A wrapper that shrink-wraps its only child carries
33
47
  // a *byte-identical* rect — measured on `cases/storybook-case`, where
@@ -37,21 +51,24 @@ export function attributeRegions(regions, snapshot, options) {
37
51
  // one: the run then names the wrapper nothing edited and sends a reviewer
38
52
  // to its file. Ties go to the last box seen, which is the innermost, and is
39
53
  // also the one the browser painted on top.
40
- if (area > 0 && shared / area >= containment) {
41
- if (inside === undefined || nodeArea <= inside.rect.width * inside.rect.height)
42
- inside = node;
54
+ const contained = added
55
+ ? spans(box.node.rect, css) && box.node.rect.width * box.node.rect.height > 0
56
+ : area > 0 && shared / area >= containment;
57
+ if (contained) {
58
+ if (inside === undefined || nodeArea <= inside.area)
59
+ inside = box;
43
60
  }
44
61
  // The same rule for `nearest`. It is orientation rather than attribution,
45
62
  // and a hint should point at the same node the join would have picked.
46
- if (overlapping === undefined || nodeArea <= overlapping.rect.width * overlapping.rect.height) {
47
- overlapping = node;
63
+ if (overlapping === undefined || nodeArea <= overlapping.area) {
64
+ overlapping = box;
48
65
  }
49
66
  }
50
67
  if (inside !== undefined) {
51
68
  return {
52
69
  region,
53
- path: inside.path,
54
- ...describe(snapshot, inside),
70
+ path: inside.node.path,
71
+ ...describe(snapshot, inside.node),
55
72
  unattributed: false,
56
73
  };
57
74
  }
@@ -59,7 +76,7 @@ export function attributeRegions(regions, snapshot, options) {
59
76
  region,
60
77
  unattributed: true,
61
78
  ...(overlapping !== undefined
62
- ? { nearest: { path: overlapping.path, ...describe(snapshot, overlapping) } }
79
+ ? { nearest: { path: overlapping.node.path, ...describe(snapshot, overlapping.node) } }
63
80
  : {}),
64
81
  };
65
82
  });
@@ -69,13 +86,13 @@ export function attributeRegions(regions, snapshot, options) {
69
86
  *
70
87
  * This function exists because of a measurement, and the measurement is the most
71
88
  * useful thing the raster tier has produced. Replacing a checkbox with a styled
72
- * div on the todomvc corpus changes 1530 pixels in 5 regions, which attribute
89
+ * div on the todomvc corpus changes 1853 pixels in 5 regions, which attribute
73
90
  * geometrically and rank by area as:
74
91
  *
75
92
  * ```
76
- * 933px Text <- changed, and reflowed
77
- * 511px Stack <- only reflowed
78
- * 86px Toggle <- the edit
93
+ * 1055px Text <- changed, and reflowed
94
+ * 614px Stack <- only reflowed
95
+ * 184px Toggle <- the edit
79
96
  * ```
80
97
  *
81
98
  * Every one of those attributions is correct. The pixels really are inside those
@@ -125,6 +142,62 @@ function describe(snapshot, node) {
125
142
  ...(node.provenance?.source ? { source: node.provenance.source } : {}),
126
143
  };
127
144
  }
145
+ /**
146
+ * The box each node in `after` had in `before`, by the node.
147
+ *
148
+ * Paired by `matchTrees`, the matcher the semantic diff reads, so a region and
149
+ * a delta never disagree about which element a node was. A path is not the key
150
+ * across renders because it is a position, and a sibling inserted ahead
151
+ * renumbers the node. A box with no area paints nothing, so it widens nothing.
152
+ */
153
+ function keptBoxes(before, after, shift) {
154
+ const kept = new Map();
155
+ if (before === undefined)
156
+ return kept;
157
+ for (const [left, arrived] of matchTrees(before.root, after.root).pairs) {
158
+ const rect = left.rect;
159
+ if (rect !== undefined && rect.width * rect.height > 0) {
160
+ kept.set(arrived, { ...rect, x: rect.x + shift.x, y: rect.y + shift.y });
161
+ }
162
+ }
163
+ return kept;
164
+ }
165
+ /**
166
+ * How much of `rect` the boxes cover, counting what two of them share once.
167
+ *
168
+ * A node's two boxes are two places, not the rectangle that holds both: a node
169
+ * that moved across a row was never in the space between, and a change there is
170
+ * not its.
171
+ */
172
+ function covered(rect, [first, second]) {
173
+ const one = intersectionArea(rect, first);
174
+ if (second === undefined)
175
+ return one;
176
+ const both = overlap(first, second);
177
+ return one + intersectionArea(rect, second) - (both === undefined ? 0 : intersectionArea(rect, both));
178
+ }
179
+ /** Whether `box`'s rows cover every row of `rect`, wherever their columns are. */
180
+ function spans(box, rect) {
181
+ return box.y <= rect.y && rect.y + rect.height <= box.y + box.height;
182
+ }
183
+ /** The area the boxes cover together, counting what two of them share once. */
184
+ function extent(rects) {
185
+ const [first, second] = rects;
186
+ if (second === undefined)
187
+ return first.width * first.height;
188
+ const x = Math.min(first.x, second.x);
189
+ const y = Math.min(first.y, second.y);
190
+ const width = Math.max(first.x + first.width, second.x + second.width) - x;
191
+ const height = Math.max(first.y + first.height, second.y + second.height) - y;
192
+ return covered({ x, y, width, height }, rects);
193
+ }
194
+ function overlap(a, b) {
195
+ const x = Math.max(a.x, b.x);
196
+ const y = Math.max(a.y, b.y);
197
+ const width = Math.min(a.x + a.width, b.x + b.width) - x;
198
+ const height = Math.min(a.y + a.height, b.y + b.height) - y;
199
+ return width > 0 && height > 0 ? { x, y, width, height } : undefined;
200
+ }
128
201
  /** Every node carrying a rect, in document order. Zero-area boxes are skipped. */
129
202
  function collectBoxes(root) {
130
203
  const out = [];