@variance-authority/core 0.15.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +428 -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":"region.js","sourceRoot":"","sources":["../../src/attribute/region.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAa,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAgJrC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEhC;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAA8B,EAC9B,QAA0B,EAC1B,OAA2B;IAE3B,MAAM,KAAK,GAAG,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IACtE,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,mBAAmB,CAAC;IAE/D,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QAC5B,MAAM,GAAG,GAAS;YAChB,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC;YACtC,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC;YACtC,KAAK,EAAE,MAAM,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK;YACnC,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK;SACtC,CAAC;QACF,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC;QAEpC,IAAI,MAAgC,CAAC;QACrC,IAAI,WAAqC,CAAC;QAE1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAK,CAAC;YACxB,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAC3C,IAAI,MAAM,IAAI,CAAC;gBAAE,SAAS;YAE1B,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;YAE1C,0EAA0E;YAC1E,2EAA2E;YAC3E,sEAAsE;YACtE,wEAAwE;YACxE,4EAA4E;YAC5E,wEAAwE;YACxE,0EAA0E;YAC1E,4EAA4E;YAC5E,2CAA2C;YAC3C,IAAI,IAAI,GAAG,CAAC,IAAI,MAAM,GAAG,IAAI,IAAI,WAAW,EAAE,CAAC;gBAC7C,IAAI,MAAM,KAAK,SAAS,IAAI,QAAQ,IAAI,MAAM,CAAC,IAAK,CAAC,KAAK,GAAG,MAAM,CAAC,IAAK,CAAC,MAAM;oBAAE,MAAM,GAAG,IAAI,CAAC;YAClG,CAAC;YACD,0EAA0E;YAC1E,uEAAuE;YACvE,IAAI,WAAW,KAAK,SAAS,IAAI,QAAQ,IAAI,WAAW,CAAC,IAAK,CAAC,KAAK,GAAG,WAAW,CAAC,IAAK,CAAC,MAAM,EAAE,CAAC;gBAChG,WAAW,GAAG,IAAI,CAAC;YACrB,CAAC;QACH,CAAC;QAED,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO;gBACL,MAAM;gBACN,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,GAAG,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAC7B,YAAY,EAAE,KAAK;aACpB,CAAC;QACJ,CAAC;QAED,OAAO;YACL,MAAM;YACN,YAAY,EAAE,IAAI;YAClB,GAAG,CAAC,WAAW,KAAK,SAAS;gBAC3B,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,IAAI,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAE,EAAE;gBAC7E,CAAC,CAAC,EAAE,CAAC;SACR,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,UAAU,WAAW,CACzB,OAAoC,EACpC,MAAM,GAAsB,EAAE;IAE9B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IAE9B,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAChB,GAAG,MAAM;QACT,yEAAyE;QACzE,sEAAsE;QACtE,qDAAqD;QACrD,KAAK,EACH,CAAC,MAAM,CAAC,SAAS,KAAK,SAAS,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC/D,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;KAC1D,CAAC,CAAC;SACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAC5F,CAAC;AAED,SAAS,QAAQ,CACf,QAA0B,EAC1B,IAAkB;IAElB,gFAAgF;IAChF,iFAAiF;IACjF,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC;IAC/C,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,SAAS,IAAI,KAAK,CAAC;IACtD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC;IAErD,OAAO;QACL,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjD,4EAA4E;QAC5E,sDAAsD;QACtD,GAAG,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,GAAG,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClC,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACvE,CAAC;AACJ,CAAC;AAED,kFAAkF;AAClF,SAAS,YAAY,CAAC,IAAkB;IACtC,MAAM,GAAG,GAAmB,EAAE,CAAC;IAE/B,MAAM,IAAI,GAAG,CAAC,IAAkB,EAAQ,EAAE;QACxC,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3F,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ;YAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC,CAAC;IACF,IAAI,CAAC,IAAI,CAAC,CAAC;IAEX,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["import { intersectionArea, type Rect } from '../format/capture.js';\nimport { locate } from './locate.js';\nimport type { DiffRegion } from './mask.js';\nimport type { NodePath, SemanticNode, SemanticSnapshot } from '../format/snapshot.js';\nimport type { SourceLocation } from '../format/provenance.js';\n\n/**\n * Isolating a pixel difference, and connecting it to the tree.\n *\n * A pixel differ answers one question — *how many pixels moved* — and that number\n * is the reason \"looks right, merge\" exists. 5482 is not a finding. It cannot be\n * read, it cannot be assigned, and the only available response to it is to open\n * the image and look, which is the expensive act the tool was supposed to replace.\n *\n * Two phases turn it into something else, and they are separate because they fail\n * differently. `mask.ts` is arithmetic on a bitmask — no DOM, no snapshot, no\n * notion of what a component is — and answers *where on the canvas*.\n * {@link attributeRegions} joins those coordinates to the box tree and answers\n * *what is there*. The first can be tested with a hand-written mask; the second\n * with a hand-written snapshot; neither needs a browser, which is what makes the\n * pipeline testable at all.\n *\n * The join is geometric and therefore the weakest link in the chain, so it is\n * explicit about failing: a region no box contains is reported `unattributed`.\n * Painting escapes its box routinely — shadows, outlines, overflowing glyphs — and\n * a system that resolved those by picking the nearest node would produce confident\n * attributions of exactly the kind an agent then acts on.\n */\n\nexport interface AttributedRegion {\n readonly region: DiffRegion;\n\n /**\n * Tightest node whose border box contains the region, when one does.\n *\n * Tightest rather than topmost: a change inside a button is also inside the\n * card and inside `<main>`, and only the innermost answer is actionable.\n */\n readonly path?: NodePath;\n readonly component?: string;\n /** Landmark phrase from {@link locate}, so a region has a name, not an address. */\n readonly where?: string;\n\n /**\n * The line of JSX that produced this node, when the build recorded one.\n *\n * The last hop, and a different answer from the one a component name gets.\n * Resolving `Button` against a source index names where `Button` is\n * *declared*; this names where the element that changed is *written*, which\n * for anything rendered more than once is the only one of the two that\n * distinguishes the instances. Present only under\n * `@variance-authority/jsx-source` or React ≤18 — the name scan is what a\n * repository that has configured nothing still gets.\n */\n readonly source?: SourceLocation;\n\n /**\n * `true` when no box contains the region at all.\n *\n * Rarer than it first looks, and that is worth being precise about. A screenshot\n * clipped to the subject means the root's own box contains every pixel in it, so\n * a region always lands *somewhere* — paint that escapes a button still falls\n * inside the card, and attributing it to the card is correct rather than a\n * consolation prize.\n *\n * What this flag actually catches is a region that fell outside the tree\n * entirely, and the overwhelmingly likely cause is that `scale` or `origin` was\n * wrong. That failure is worth a loud signal precisely because it does not look\n * like one: at the wrong scale every region lands in the top-left quadrant and\n * the report comes out complete, plausible, and about the wrong components.\n */\n readonly unattributed: boolean;\n\n /**\n * Tightest node the region *overlaps*, present only when unattributed.\n *\n * Offered as orientation, never as attribution — hence the separate field. It\n * answers \"what is this near\", which is a different claim from \"this is what\n * changed\", and keeping them in one field is how the second gets asserted with\n * the confidence of the first.\n */\n readonly nearest?: { readonly path: NodePath; readonly component?: string; readonly where?: string };\n\n /**\n * Nearest enclosing component, when it is not the one that authored the node.\n *\n * Two namespaces meet here and neither is wrong. `component` is `createdBy` —\n * who wrote the JSX (ADR-0007) — and a component *hash* is named for the\n * enclosure (ADR-0018). They diverge exactly where an element is passed as a\n * prop: `<Card title={<h3>Invoice</h3>} />` gives the `<h3>` `createdBy: Page`\n * and `owners[0]: Card`.\n *\n * Carried so that ranking can match a cause list without guessing which of the\n * two it is written in. Matching one namespace against the other does not\n * fail loudly — it silently finds nothing, and the ordering falls back to area,\n * which is the thing the cause list exists to prevent.\n */\n readonly owner?: string;\n\n /**\n * The shape of this difference, with its position and values removed.\n *\n * Filled in by whoever held the mask — `attributeRegions` never sets it,\n * because a fingerprint is computed from pixels and this function is given\n * boxes. It exists on this type rather than beside it so that the digest\n * travels with the region a reader is looking at: writing an ignore means\n * copying the fingerprint of the thing that annoyed you, and a digest printed\n * in a different section is a digest nobody matches up.\n *\n * See `fingerprintOfMask` in `core/judge`.\n */\n readonly fingerprint?: string;\n}\n\nexport interface AttributionOptions {\n /**\n * Device pixels per CSS pixel in the raster.\n *\n * Required, with no default. A 2x screenshot silently attributed at 1x lands\n * every region in the top-left quadrant of the page and attributes each of them\n * to the wrong node — a failure that produces a full, plausible, entirely wrong\n * report. Making the caller state it turns that into a decision someone made.\n */\n readonly scale: number;\n\n /**\n * Page-space CSS origin of the raster's top-left corner.\n *\n * Defaults to the subject root's own rect, which is right when the screenshot\n * was clipped to the subject — the normal case. A full-page shot passes `{x: 0,\n * y: 0}`.\n */\n readonly origin?: { readonly x: number; readonly y: number };\n\n /**\n * Fraction of a region's area that must fall inside a box to count as inside.\n *\n * Below 1 by default because a region's bounding box is inflated by\n * antialiasing at its edges, so exact containment would reject the correct node\n * on a text change. This is the one genuinely fuzzy number in the join and it\n * is named rather than buried.\n */\n readonly containment?: number;\n}\n\nconst DEFAULT_CONTAINMENT = 0.9;\n\n/**\n * Join isolated regions to the nodes that occupy them.\n *\n * Requires a snapshot carrying `rect`, i.e. a profile with layout (ADR-0002).\n * Under a profile without one every region comes back unattributed, which is the\n * correct answer — a rect that was never observed must never be inferred.\n */\nexport function attributeRegions(\n regions: readonly DiffRegion[],\n snapshot: SemanticSnapshot,\n options: AttributionOptions,\n): readonly AttributedRegion[] {\n const boxed = collectBoxes(snapshot.root);\n const origin = options.origin ?? snapshot.root.rect ?? { x: 0, y: 0 };\n const containment = options.containment ?? DEFAULT_CONTAINMENT;\n\n return regions.map((region) => {\n const css: Rect = {\n x: region.x / options.scale + origin.x,\n y: region.y / options.scale + origin.y,\n width: region.width / options.scale,\n height: region.height / options.scale,\n };\n const area = css.width * css.height;\n\n let inside: SemanticNode | undefined;\n let overlapping: SemanticNode | undefined;\n\n for (const node of boxed) {\n const rect = node.rect!;\n const shared = intersectionArea(css, rect);\n if (shared <= 0) continue;\n\n const nodeArea = rect.width * rect.height;\n\n // `<=`, not `<`, and the difference is one component name in every report\n // over a design system. A wrapper that shrink-wraps its only child carries\n // a *byte-identical* rect — measured on `cases/storybook-case`, where\n // `Tokens` and `Button` are both `454.34,359 115.33×50` — so neither is\n // tighter than the other and the walk decides. `collectBoxes` is pre-order,\n // so a strict `<` keeps whichever came first, which is always the outer\n // one: the run then names the wrapper nothing edited and sends a reviewer\n // to its file. Ties go to the last box seen, which is the innermost, and is\n // also the one the browser painted on top.\n if (area > 0 && shared / area >= containment) {\n if (inside === undefined || nodeArea <= inside.rect!.width * inside.rect!.height) inside = node;\n }\n // The same rule for `nearest`. It is orientation rather than attribution,\n // and a hint should point at the same node the join would have picked.\n if (overlapping === undefined || nodeArea <= overlapping.rect!.width * overlapping.rect!.height) {\n overlapping = node;\n }\n }\n\n if (inside !== undefined) {\n return {\n region,\n path: inside.path,\n ...describe(snapshot, inside),\n unattributed: false,\n };\n }\n\n return {\n region,\n unattributed: true,\n ...(overlapping !== undefined\n ? { nearest: { path: overlapping.path, ...describe(snapshot, overlapping) } }\n : {}),\n };\n });\n}\n\nexport interface RankedRegion extends AttributedRegion {\n /**\n * `true` when the semantic tier named this region's component as a *root* of\n * the change, rather than as something the change happened to move.\n */\n readonly cause: boolean;\n}\n\n/**\n * Rank attributed regions by cause, falling back to area.\n *\n * This function exists because of a measurement, and the measurement is the most\n * useful thing the raster tier has produced. Replacing a checkbox with a styled\n * div on the todomvc corpus changes 1530 pixels in 5 regions, which attribute\n * geometrically and rank by area as:\n *\n * ```\n * 933px Text <- changed, and reflowed\n * 511px Stack <- only reflowed\n * 86px Toggle <- the edit\n * ```\n *\n * Every one of those attributions is correct. The pixels really are inside those\n * nodes. The *ordering* is still wrong, because **area measures displacement,\n * not cause** — an edit that reflows its surroundings moves far more of them than\n * of itself, so `Stack`, which nothing edited, outranks `Toggle`, which is the\n * edit. That is the same defect the differ had when a list reorder blamed the\n * element that moved rather than the code that moved it.\n *\n * Geometry cannot fix this; it has no access to why. The semantic tier does — it\n * has provenance and props digests — so the raster tier stops claiming to rank\n * and takes the ordering from the tier that can. On the case above that promotes\n * `Toggle` above `Stack` while leaving both attributions untouched.\n *\n * Two limits, stated rather than smoothed over. The semantic tier may name more\n * than one cause and here it names two, `Toggle` and `Text`, both real — this\n * does not collapse them, because picking one would be inventing a fact. And\n * with no causes supplied the order falls back to area, which is honest and is\n * not good: pixels alone rank the displaced above the displacer, and nothing\n * inside this file can change that.\n */\nexport function rankRegions(\n regions: readonly AttributedRegion[],\n causes: readonly string[] = [],\n): readonly RankedRegion[] {\n const named = new Set(causes);\n\n return regions\n .map((region) => ({\n ...region,\n // Either namespace. A cause list is named for enclosures and a region is\n // named for its author, and where those differ a one-sided test finds\n // nothing and silently reverts the ordering to area.\n cause:\n (region.component !== undefined && named.has(region.component)) ||\n (region.owner !== undefined && named.has(region.owner)),\n }))\n .sort((a, b) => Number(b.cause) - Number(a.cause) || b.region.pixels - a.region.pixels);\n}\n\nfunction describe(\n snapshot: SemanticSnapshot,\n node: SemanticNode,\n): { component?: string; owner?: string; where?: string; source?: SourceLocation } {\n // `createdBy` before `owners[0]`: the component whose JSX produced this element\n // owns its appearance, while the nearest enclosing component merely contains it.\n const owner = node.provenance?.owners[0]?.name;\n const component = node.provenance?.createdBy ?? owner;\n const where = locate(snapshot.root, node.path).where;\n\n return {\n ...(component !== undefined ? { component } : {}),\n // Only when it says something `component` does not. An `owner` that repeats\n // the author is noise in every report that prints it.\n ...(owner !== undefined && owner !== component ? { owner } : {}),\n ...(where !== '' ? { where } : {}),\n ...(node.provenance?.source ? { source: node.provenance.source } : {}),\n };\n}\n\n/** Every node carrying a rect, in document order. Zero-area boxes are skipped. */\nfunction collectBoxes(root: SemanticNode): readonly SemanticNode[] {\n const out: SemanticNode[] = [];\n\n const walk = (node: SemanticNode): void => {\n if (node.rect !== undefined && node.rect.width > 0 && node.rect.height > 0) out.push(node);\n for (const child of node.children) walk(child);\n };\n walk(root);\n\n return out;\n}\n"]}
1
+ {"version":3,"file":"region.js","sourceRoot":"","sources":["../../src/attribute/region.ts"],"names":[],"mappings":"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","sourcesContent":["import { intersectionArea, type Rect } from '../format/capture.js';\nimport { locate } from './locate.js';\nimport { matchTrees } from '../compare/diff/match.js';\nimport type { DiffRegion } from './mask.js';\nimport type { NodePath, SemanticNode, SemanticSnapshot } from '../format/snapshot.js';\nimport type { SourceLocation } from '../format/provenance.js';\n\n/**\n * Isolating a pixel difference, and connecting it to the tree.\n *\n * A pixel differ answers one question — *how many pixels moved* — and that number\n * is the reason \"looks right, merge\" exists. 5482 is not a finding. It cannot be\n * read, it cannot be assigned, and the only available response to it is to open\n * the image and look, which is the expensive act the tool was supposed to replace.\n *\n * Two phases turn it into something else, and they are separate because they fail\n * differently. `mask.ts` is arithmetic on a bitmask — no DOM, no snapshot, no\n * notion of what a component is — and answers *where on the canvas*.\n * {@link attributeRegions} joins those coordinates to the box tree and answers\n * *what is there*. The first can be tested with a hand-written mask; the second\n * with a hand-written snapshot; neither needs a browser, which is what makes the\n * pipeline testable at all.\n *\n * The join is geometric and therefore the weakest link in the chain, so it is\n * explicit about failing: a region no box contains is reported `unattributed`.\n * Painting escapes its box routinely — shadows, outlines, overflowing glyphs — and\n * a system that resolved those by picking the nearest node would produce confident\n * attributions of exactly the kind an agent then acts on.\n */\n\nexport interface AttributedRegion {\n readonly region: DiffRegion;\n\n /**\n * Tightest node whose border box contains the region, when one does.\n *\n * Tightest rather than topmost: a change inside a button is also inside the\n * card and inside `<main>`, and only the innermost answer is actionable.\n */\n readonly path?: NodePath;\n readonly component?: string;\n /** Landmark phrase from {@link locate}, so a region has a name, not an address. */\n readonly where?: string;\n\n /**\n * The line of JSX that produced this node, when the build recorded one.\n *\n * The last hop, and a different answer from the one a component name gets.\n * Resolving `Button` against a source index names where `Button` is\n * *declared*; this names where the element that changed is *written*, which\n * for anything rendered more than once is the only one of the two that\n * distinguishes the instances. Present only under\n * `@variance-authority/jsx-source` or React ≤18 — the name scan is what a\n * repository that has configured nothing still gets.\n */\n readonly source?: SourceLocation;\n\n /**\n * `true` when no box contains the region at all.\n *\n * Rarer than it first looks, and that is worth being precise about. A screenshot\n * clipped to the subject means the root's own box contains every pixel in it, so\n * a region always lands *somewhere* — paint that escapes a button still falls\n * inside the card, and attributing it to the card is correct rather than a\n * consolation prize.\n *\n * What this flag actually catches is a region that fell outside the tree\n * entirely, and the overwhelmingly likely cause is that `scale` or `origin` was\n * wrong. That failure is worth a loud signal precisely because it does not look\n * like one: at the wrong scale every region lands in the top-left quadrant and\n * the report comes out complete, plausible, and about the wrong components.\n */\n readonly unattributed: boolean;\n\n /**\n * Tightest node the region *overlaps*, present only when unattributed.\n *\n * Offered as orientation, never as attribution — hence the separate field. It\n * answers \"what is this near\", which is a different claim from \"this is what\n * changed\", and keeping them in one field is how the second gets asserted with\n * the confidence of the first.\n */\n readonly nearest?: { readonly path: NodePath; readonly component?: string; readonly where?: string };\n\n /**\n * Nearest enclosing component, when it is not the one that authored the node.\n *\n * Two namespaces meet here and neither is wrong. `component` is `createdBy` —\n * who wrote the JSX (ADR-0007) — and a component *hash* is named for the\n * enclosure (ADR-0018). They diverge exactly where an element is passed as a\n * prop: `<Card title={<h3>Invoice</h3>} />` gives the `<h3>` `createdBy: Page`\n * and `owners[0]: Card`.\n *\n * Carried so that ranking can match a cause list without guessing which of the\n * two it is written in. Matching one namespace against the other does not\n * fail loudly — it silently finds nothing, and the ordering falls back to area,\n * which is the thing the cause list exists to prevent.\n */\n readonly owner?: string;\n\n /**\n * The shape of this difference, with its position and values removed.\n *\n * Filled in by whoever held the mask — `attributeRegions` never sets it,\n * because a fingerprint is computed from pixels and this function is given\n * boxes. It exists on this type rather than beside it so that the digest\n * travels with the region a reader is looking at: writing an ignore means\n * copying the fingerprint of the thing that annoyed you, and a digest printed\n * in a different section is a digest nobody matches up.\n *\n * See `fingerprintOfMask` in `core/judge`.\n */\n readonly fingerprint?: string;\n}\n\nexport interface AttributionOptions {\n /**\n * Device pixels per CSS pixel in the raster.\n *\n * Required, with no default. A 2x screenshot silently attributed at 1x lands\n * every region in the top-left quadrant of the page and attributes each of them\n * to the wrong node — a failure that produces a full, plausible, entirely wrong\n * report. Making the caller state it turns that into a decision someone made.\n */\n readonly scale: number;\n\n /**\n * Page-space CSS origin of the raster's top-left corner.\n *\n * Defaults to the subject root's own rect, which is right when the screenshot\n * was clipped to the subject — the normal case. A full-page shot passes `{x: 0,\n * y: 0}`.\n */\n readonly origin?: { readonly x: number; readonly y: number };\n\n /**\n * Fraction of a region's area that must fall inside a box to count as inside.\n *\n * Below 1 by default because a region's bounding box is inflated by\n * antialiasing at its edges, so exact containment would reject the correct node\n * on a text change. This is the one genuinely fuzzy number in the join and it\n * is named rather than buried.\n */\n readonly containment?: number;\n\n /**\n * The snapshot of the render the pixels are compared against.\n *\n * Required, and `undefined` is an answer: it says the other render is not\n * known. A changed region is the union of what left and what arrived, so each\n * node is joined against its box in this render and the box of the node the\n * differ's matcher pairs it with in that one. Without it a control replaced\n * by a larger element changes pixels outside either box alone, and the join\n * names the row it sits in.\n *\n * Its boxes are placed by where they were in its own image, whose corner is\n * {@link beforeOrigin}. A shot clipped to the subject starts at its root, so\n * a root the page moved between the two shots moves none of the pixels, and\n * each box that left is shifted by it.\n */\n readonly before: SemanticSnapshot | undefined;\n\n /**\n * Page-space CSS origin of the before image's top-left corner.\n *\n * Defaults to `before`'s own root rect, as `origin` defaults to this\n * render's. Required when `origin` is stated and `before` is a snapshot: two\n * shots clipped to anything but the root share a corner only by the page's\n * layout, and a corner guessed from the other image gives a plausible report\n * about the wrong components. A full-page pair passes `{x: 0, y: 0}` for both.\n */\n readonly beforeOrigin?: { readonly x: number; readonly y: number };\n\n /**\n * The rows of this image that have no partner in the before image, as\n * half-open runs of raster rows.\n *\n * Such a row is marked edge to edge, because nothing says where its content\n * begins, so a region inside one is as wide as the image and only the root\n * contains it by area. It is joined by its rows instead: to the tightest box\n * whose rows span it, which is the box that grew. Absent when the rows were\n * compared by position.\n */\n readonly inserted?: readonly { readonly from: number; readonly to: number }[];\n}\n\nconst DEFAULT_CONTAINMENT = 0.9;\n\n/**\n * Join isolated regions to the nodes that occupy them.\n *\n * Requires a snapshot carrying `rect`, i.e. a profile with layout (ADR-0002).\n * Under a profile without one every region comes back unattributed, which is the\n * correct answer — a rect that was never observed must never be inferred.\n */\nexport function attributeRegions(\n regions: readonly DiffRegion[],\n snapshot: SemanticSnapshot,\n options: AttributionOptions,\n): readonly AttributedRegion[] {\n if (options.origin !== undefined && options.before !== undefined && options.beforeOrigin === undefined) {\n throw new Error(\n '`origin` is stated and `beforeOrigin` is not: state the corner of the before image too, ' +\n 'since two shots share one only when the page did not move between them',\n );\n }\n const origin = options.origin ?? snapshot.root.rect ?? { x: 0, y: 0 };\n // A box that left is placed by where it was in its image, not on the page.\n const was = options.beforeOrigin ?? options.before?.root.rect ?? origin;\n const left = keptBoxes(options.before, snapshot, { x: origin.x - was.x, y: origin.y - was.y });\n const boxed = collectBoxes(snapshot.root).map((node) => {\n const before = left.get(node);\n const rects = before === undefined ? [node.rect!] : [node.rect!, before];\n return { node, rects, area: extent(rects) };\n });\n const containment = options.containment ?? DEFAULT_CONTAINMENT;\n\n return regions.map((region) => {\n const css: Rect = {\n x: region.x / options.scale + origin.x,\n y: region.y / options.scale + origin.y,\n width: region.width / options.scale,\n height: region.height / options.scale,\n };\n const area = css.width * css.height;\n // Rows the after image added are joined by the rows they span, and only to\n // this render's box: the before side never painted them.\n const added = options.inserted?.some((run) => run.from <= region.y && region.y + region.height <= run.to) ?? false;\n\n let inside: (typeof boxed)[number] | undefined;\n let overlapping: (typeof boxed)[number] | undefined;\n\n for (const box of boxed) {\n const shared = covered(css, box.rects);\n if (shared <= 0) continue;\n\n const nodeArea = box.area;\n\n // `<=`, not `<`, and the difference is one component name in every report\n // over a design system. A wrapper that shrink-wraps its only child carries\n // a *byte-identical* rect — measured on `cases/storybook-case`, where\n // `Tokens` and `Button` are both `454.34,359 115.33×50` — so neither is\n // tighter than the other and the walk decides. `collectBoxes` is pre-order,\n // so a strict `<` keeps whichever came first, which is always the outer\n // one: the run then names the wrapper nothing edited and sends a reviewer\n // to its file. Ties go to the last box seen, which is the innermost, and is\n // also the one the browser painted on top.\n const contained = added\n ? spans(box.node.rect!, css) && box.node.rect!.width * box.node.rect!.height > 0\n : area > 0 && shared / area >= containment;\n if (contained) {\n if (inside === undefined || nodeArea <= inside.area) inside = box;\n }\n // The same rule for `nearest`. It is orientation rather than attribution,\n // and a hint should point at the same node the join would have picked.\n if (overlapping === undefined || nodeArea <= overlapping.area) {\n overlapping = box;\n }\n }\n\n if (inside !== undefined) {\n return {\n region,\n path: inside.node.path,\n ...describe(snapshot, inside.node),\n unattributed: false,\n };\n }\n\n return {\n region,\n unattributed: true,\n ...(overlapping !== undefined\n ? { nearest: { path: overlapping.node.path, ...describe(snapshot, overlapping.node) } }\n : {}),\n };\n });\n}\n\nexport interface RankedRegion extends AttributedRegion {\n /**\n * `true` when the semantic tier named this region's component as a *root* of\n * the change, rather than as something the change happened to move.\n */\n readonly cause: boolean;\n}\n\n/**\n * Rank attributed regions by cause, falling back to area.\n *\n * This function exists because of a measurement, and the measurement is the most\n * useful thing the raster tier has produced. Replacing a checkbox with a styled\n * div on the todomvc corpus changes 1853 pixels in 5 regions, which attribute\n * geometrically and rank by area as:\n *\n * ```\n * 1055px Text <- changed, and reflowed\n * 614px Stack <- only reflowed\n * 184px Toggle <- the edit\n * ```\n *\n * Every one of those attributions is correct. The pixels really are inside those\n * nodes. The *ordering* is still wrong, because **area measures displacement,\n * not cause** — an edit that reflows its surroundings moves far more of them than\n * of itself, so `Stack`, which nothing edited, outranks `Toggle`, which is the\n * edit. That is the same defect the differ had when a list reorder blamed the\n * element that moved rather than the code that moved it.\n *\n * Geometry cannot fix this; it has no access to why. The semantic tier does — it\n * has provenance and props digests — so the raster tier stops claiming to rank\n * and takes the ordering from the tier that can. On the case above that promotes\n * `Toggle` above `Stack` while leaving both attributions untouched.\n *\n * Two limits, stated rather than smoothed over. The semantic tier may name more\n * than one cause and here it names two, `Toggle` and `Text`, both real — this\n * does not collapse them, because picking one would be inventing a fact. And\n * with no causes supplied the order falls back to area, which is honest and is\n * not good: pixels alone rank the displaced above the displacer, and nothing\n * inside this file can change that.\n */\nexport function rankRegions(\n regions: readonly AttributedRegion[],\n causes: readonly string[] = [],\n): readonly RankedRegion[] {\n const named = new Set(causes);\n\n return regions\n .map((region) => ({\n ...region,\n // Either namespace. A cause list is named for enclosures and a region is\n // named for its author, and where those differ a one-sided test finds\n // nothing and silently reverts the ordering to area.\n cause:\n (region.component !== undefined && named.has(region.component)) ||\n (region.owner !== undefined && named.has(region.owner)),\n }))\n .sort((a, b) => Number(b.cause) - Number(a.cause) || b.region.pixels - a.region.pixels);\n}\n\nfunction describe(\n snapshot: SemanticSnapshot,\n node: SemanticNode,\n): { component?: string; owner?: string; where?: string; source?: SourceLocation } {\n // `createdBy` before `owners[0]`: the component whose JSX produced this element\n // owns its appearance, while the nearest enclosing component merely contains it.\n const owner = node.provenance?.owners[0]?.name;\n const component = node.provenance?.createdBy ?? owner;\n const where = locate(snapshot.root, node.path).where;\n\n return {\n ...(component !== undefined ? { component } : {}),\n // Only when it says something `component` does not. An `owner` that repeats\n // the author is noise in every report that prints it.\n ...(owner !== undefined && owner !== component ? { owner } : {}),\n ...(where !== '' ? { where } : {}),\n ...(node.provenance?.source ? { source: node.provenance.source } : {}),\n };\n}\n\n/**\n * The box each node in `after` had in `before`, by the node.\n *\n * Paired by `matchTrees`, the matcher the semantic diff reads, so a region and\n * a delta never disagree about which element a node was. A path is not the key\n * across renders because it is a position, and a sibling inserted ahead\n * renumbers the node. A box with no area paints nothing, so it widens nothing.\n */\nfunction keptBoxes(\n before: SemanticSnapshot | undefined,\n after: SemanticSnapshot,\n shift: { readonly x: number; readonly y: number },\n): ReadonlyMap<SemanticNode, Rect> {\n const kept = new Map<SemanticNode, Rect>();\n if (before === undefined) return kept;\n for (const [left, arrived] of matchTrees(before.root, after.root).pairs) {\n const rect = left.rect;\n if (rect !== undefined && rect.width * rect.height > 0) {\n kept.set(arrived, { ...rect, x: rect.x + shift.x, y: rect.y + shift.y });\n }\n }\n return kept;\n}\n\n/**\n * How much of `rect` the boxes cover, counting what two of them share once.\n *\n * A node's two boxes are two places, not the rectangle that holds both: a node\n * that moved across a row was never in the space between, and a change there is\n * not its.\n */\nfunction covered(rect: Rect, [first, second]: readonly Rect[]): number {\n const one = intersectionArea(rect, first!);\n if (second === undefined) return one;\n const both = overlap(first!, second);\n return one + intersectionArea(rect, second) - (both === undefined ? 0 : intersectionArea(rect, both));\n}\n\n/** Whether `box`'s rows cover every row of `rect`, wherever their columns are. */\nfunction spans(box: Rect, rect: Rect): boolean {\n return box.y <= rect.y && rect.y + rect.height <= box.y + box.height;\n}\n\n/** The area the boxes cover together, counting what two of them share once. */\nfunction extent(rects: readonly Rect[]): number {\n const [first, second] = rects;\n if (second === undefined) return first!.width * first!.height;\n const x = Math.min(first!.x, second.x);\n const y = Math.min(first!.y, second.y);\n const width = Math.max(first!.x + first!.width, second.x + second.width) - x;\n const height = Math.max(first!.y + first!.height, second.y + second.height) - y;\n return covered({ x, y, width, height }, rects);\n}\n\nfunction overlap(a: Rect, b: Rect): Rect | undefined {\n const x = Math.max(a.x, b.x);\n const y = Math.max(a.y, b.y);\n const width = Math.min(a.x + a.width, b.x + b.width) - x;\n const height = Math.min(a.y + a.height, b.y + b.height) - y;\n return width > 0 && height > 0 ? { x, y, width, height } : undefined;\n}\n\n/** Every node carrying a rect, in document order. Zero-area boxes are skipped. */\nfunction collectBoxes(root: SemanticNode): readonly SemanticNode[] {\n const out: SemanticNode[] = [];\n\n const walk = (node: SemanticNode): void => {\n if (node.rect !== undefined && node.rect.width > 0 && node.rect.height > 0) out.push(node);\n for (const child of node.children) walk(child);\n };\n walk(root);\n\n return out;\n}\n"]}
@@ -23,7 +23,7 @@
23
23
  *
24
24
  * The index is plain data. `core` performs no I/O (ADR-0006) and has no parser,
25
25
  * so building one is a caller's job — `indexDeclarations` in
26
- * [`@variance-authority/sense`](../../../sense/src/declarations.ts) reads it off
26
+ * [`@variance-authority/sense`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/sense/src/declarations.ts) reads it off
27
27
  * a module's parse — and resolving against one is here.
28
28
  */
29
29
  export interface SourceRef {
@@ -23,7 +23,7 @@
23
23
  *
24
24
  * The index is plain data. `core` performs no I/O (ADR-0006) and has no parser,
25
25
  * so building one is a caller's job — `indexDeclarations` in
26
- * [`@variance-authority/sense`](../../../sense/src/declarations.ts) reads it off
26
+ * [`@variance-authority/sense`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/sense/src/declarations.ts) reads it off
27
27
  * a module's parse — and resolving against one is here.
28
28
  */
29
29
  export function resolveSource(name, index) {
@@ -1 +1 @@
1
- {"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/attribute/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAoCH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,KAAkB;IAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;AACpD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,UAAsB;IACjD,MAAM,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC;IAChC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,IAAI,CAAC;IAEhD,MAAM,OAAO,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,SAAS;QAAE,OAAO,OAAO,CAAC;IAE1C,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAChF,OAAO,GAAG,OAAO,sBAAsB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAC9D,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,kBAAkB,CAAC,OAA+B;IAChE,MAAM,MAAM,GAAgC,EAAE,CAAC;IAE/C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjD,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAiB,EAAE,OAAoB;IACxE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC;AACjC,CAAC","sourcesContent":["/**\n * Connecting a change to a file.\n *\n * A report that says `Button` is one step short of useful. An agent asked to fix\n * it still has to find `Button`, and a reviewer still has to guess whether the\n * `Button` in question is the design-system one or the local one in checkout.\n * The last hop — **which file** — is what turns a finding into an edit.\n *\n * There are two mechanisms, and this file is the second one.\n *\n * The first is exact. Spec §6.1 assumes the build tells us: every JSX transform\n * already computes each element's file, line and column, and\n * `@variance-authority/jsx-source` is the `jsxImportSource` setting that keeps\n * that location as far as the fiber — React 19 drops it otherwise, on every path.\n * When it is on, a finding names the element's own line and this index is not\n * consulted.\n *\n * The second is this one, and it is what a repository that has changed nothing\n * gets. Attribution names *components*, so a **component → file** index answers\n * the question attribution asks. It is built by reading source rather than by\n * configuring a build, and it answers with where a component is *declared* —\n * coarser than a call site, and enough to open the right file.\n *\n * The index is plain data. `core` performs no I/O (ADR-0006) and has no parser,\n * so building one is a caller's job — `indexDeclarations` in\n * [`@variance-authority/sense`](../../../sense/src/declarations.ts) reads it off\n * a module's parse — and resolving against one is here.\n */\n\nexport interface SourceRef {\n /** Repository-relative, so a report is portable between machines and CI. */\n readonly file: string;\n readonly line: number;\n /**\n * How the declaration was recognised. Carried so a bad match is debuggable.\n *\n * The first three say which kind of statement declared the name; a `let`\n * binding is shown as `const`. `engine` is the one that is not read from\n * source at all: the browser was asked where the function it rendered was\n * compiled from, and that position was mapped back to the file. It names the\n * component that ran, whatever it was called.\n */\n readonly via: 'function' | 'const' | 'class' | 'engine';\n}\n\n/**\n * Component name → where it is declared.\n *\n * A name may map to several files. That is not an error to be resolved by\n * picking one: two components genuinely can share a name, and silently choosing\n * the first would send an agent to edit the wrong file with full confidence.\n * Ambiguity is reported (ADR-0003's rule for overlapping projections, applied to\n * source).\n */\nexport type SourceIndex = Readonly<Record<string, readonly SourceRef[]>>;\n\nexport interface Resolution {\n readonly name: string;\n readonly refs: readonly SourceRef[];\n /** `true` when more than one file declares this name. */\n readonly ambiguous: boolean;\n}\n\nexport function resolveSource(name: string, index: SourceIndex): Resolution | null {\n const refs = index[name];\n if (refs === undefined || refs.length === 0) return null;\n\n return { name, refs, ambiguous: refs.length > 1 };\n}\n\n/**\n * `file:line`, or a disambiguating list when a name is declared more than once.\n *\n * Formatted as `path:line` because that is the form an editor, a terminal, and\n * every agent harness already know how to open.\n */\nexport function formatSource(resolution: Resolution): string {\n const [first] = resolution.refs;\n if (first === undefined) return resolution.name;\n\n const primary = `${first.file}:${first.line}`;\n if (!resolution.ambiguous) return primary;\n\n const others = resolution.refs.slice(1).map((ref) => `${ref.file}:${ref.line}`);\n return `${primary} (ambiguous — also ${others.join(', ')})`;\n}\n\n/** Merge per-file indexes. A name declared in several files keeps every ref. */\nexport function mergeSourceIndexes(indexes: readonly SourceIndex[]): SourceIndex {\n const merged: Record<string, SourceRef[]> = {};\n\n for (const index of indexes) {\n for (const [name, refs] of Object.entries(index)) {\n (merged[name] ??= []).push(...refs);\n }\n }\n\n return merged;\n}\n\n/**\n * One index laid over another: a name the overlay holds replaces the base's refs.\n *\n * Not a merge. A scan finds every file that declares a `Button`, and reports the\n * pair as ambiguous because the text cannot say which one a page rendered. The\n * engine can, and when it has answered for a name the scan's guesses for that\n * name are no longer the question. A name the overlay never saw keeps what the\n * base had, so a component the page did not mount still has a file.\n */\nexport function overlaySourceIndex(base: SourceIndex, overlay: SourceIndex): SourceIndex {\n return { ...base, ...overlay };\n}\n"]}
1
+ {"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/attribute/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAoCH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,KAAkB;IAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;AACpD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,UAAsB;IACjD,MAAM,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC;IAChC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,IAAI,CAAC;IAEhD,MAAM,OAAO,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,SAAS;QAAE,OAAO,OAAO,CAAC;IAE1C,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAChF,OAAO,GAAG,OAAO,sBAAsB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAC9D,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,kBAAkB,CAAC,OAA+B;IAChE,MAAM,MAAM,GAAgC,EAAE,CAAC;IAE/C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjD,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAiB,EAAE,OAAoB;IACxE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC;AACjC,CAAC","sourcesContent":["/**\n * Connecting a change to a file.\n *\n * A report that says `Button` is one step short of useful. An agent asked to fix\n * it still has to find `Button`, and a reviewer still has to guess whether the\n * `Button` in question is the design-system one or the local one in checkout.\n * The last hop — **which file** — is what turns a finding into an edit.\n *\n * There are two mechanisms, and this file is the second one.\n *\n * The first is exact. Spec §6.1 assumes the build tells us: every JSX transform\n * already computes each element's file, line and column, and\n * `@variance-authority/jsx-source` is the `jsxImportSource` setting that keeps\n * that location as far as the fiber — React 19 drops it otherwise, on every path.\n * When it is on, a finding names the element's own line and this index is not\n * consulted.\n *\n * The second is this one, and it is what a repository that has changed nothing\n * gets. Attribution names *components*, so a **component → file** index answers\n * the question attribution asks. It is built by reading source rather than by\n * configuring a build, and it answers with where a component is *declared* —\n * coarser than a call site, and enough to open the right file.\n *\n * The index is plain data. `core` performs no I/O (ADR-0006) and has no parser,\n * so building one is a caller's job — `indexDeclarations` in\n * [`@variance-authority/sense`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/sense/src/declarations.ts) reads it off\n * a module's parse — and resolving against one is here.\n */\n\nexport interface SourceRef {\n /** Repository-relative, so a report is portable between machines and CI. */\n readonly file: string;\n readonly line: number;\n /**\n * How the declaration was recognised. Carried so a bad match is debuggable.\n *\n * The first three say which kind of statement declared the name; a `let`\n * binding is shown as `const`. `engine` is the one that is not read from\n * source at all: the browser was asked where the function it rendered was\n * compiled from, and that position was mapped back to the file. It names the\n * component that ran, whatever it was called.\n */\n readonly via: 'function' | 'const' | 'class' | 'engine';\n}\n\n/**\n * Component name → where it is declared.\n *\n * A name may map to several files. That is not an error to be resolved by\n * picking one: two components genuinely can share a name, and silently choosing\n * the first would send an agent to edit the wrong file with full confidence.\n * Ambiguity is reported (ADR-0003's rule for overlapping projections, applied to\n * source).\n */\nexport type SourceIndex = Readonly<Record<string, readonly SourceRef[]>>;\n\nexport interface Resolution {\n readonly name: string;\n readonly refs: readonly SourceRef[];\n /** `true` when more than one file declares this name. */\n readonly ambiguous: boolean;\n}\n\nexport function resolveSource(name: string, index: SourceIndex): Resolution | null {\n const refs = index[name];\n if (refs === undefined || refs.length === 0) return null;\n\n return { name, refs, ambiguous: refs.length > 1 };\n}\n\n/**\n * `file:line`, or a disambiguating list when a name is declared more than once.\n *\n * Formatted as `path:line` because that is the form an editor, a terminal, and\n * every agent harness already know how to open.\n */\nexport function formatSource(resolution: Resolution): string {\n const [first] = resolution.refs;\n if (first === undefined) return resolution.name;\n\n const primary = `${first.file}:${first.line}`;\n if (!resolution.ambiguous) return primary;\n\n const others = resolution.refs.slice(1).map((ref) => `${ref.file}:${ref.line}`);\n return `${primary} (ambiguous — also ${others.join(', ')})`;\n}\n\n/** Merge per-file indexes. A name declared in several files keeps every ref. */\nexport function mergeSourceIndexes(indexes: readonly SourceIndex[]): SourceIndex {\n const merged: Record<string, SourceRef[]> = {};\n\n for (const index of indexes) {\n for (const [name, refs] of Object.entries(index)) {\n (merged[name] ??= []).push(...refs);\n }\n }\n\n return merged;\n}\n\n/**\n * One index laid over another: a name the overlay holds replaces the base's refs.\n *\n * Not a merge. A scan finds every file that declares a `Button`, and reports the\n * pair as ambiguous because the text cannot say which one a page rendered. The\n * engine can, and when it has answered for a name the scan's guesses for that\n * name are no longer the question. A name the overlay never saw keeps what the\n * base had, so a component the page did not mount still has a file.\n */\nexport function overlaySourceIndex(base: SourceIndex, overlay: SourceIndex): SourceIndex {\n return { ...base, ...overlay };\n}\n"]}
@@ -59,7 +59,7 @@ export declare const BANDS: readonly Band[];
59
59
  */
60
60
  export declare function loudestBand(bands: Iterable<Band>): Band | null;
61
61
  /** What kind of thing changed, before policy has an opinion about it. */
62
- export type DeltaKind = 'node-added' | 'node-removed' | 'node-moved' | 'role-changed' | 'name-changed' | 'description-changed' | 'state-changed' | 'text-changed' | 'attribute-changed' | 'rect-changed' | 'style-changed' | 'token-changed' | 'raster-residue';
62
+ export type DeltaKind = 'node-added' | 'node-removed' | 'node-moved' | 'tag-changed' | 'role-changed' | 'name-changed' | 'description-changed' | 'state-changed' | 'text-changed' | 'attribute-changed' | 'rect-changed' | 'style-changed' | 'token-changed' | 'raster-residue';
63
63
  /**
64
64
  * The band a delta falls in, before policy.
65
65
  *
@@ -78,8 +78,14 @@ export type DeltaKind = 'node-added' | 'node-removed' | 'node-moved' | 'role-cha
78
78
  * there is no rect evidence at all, and the style change stands alone — which is
79
79
  * why JSDOM can decide the token band but only part of geometry.
80
80
  *
81
+ * A style delta on a pair across two tags, `retagged`, is `geometry`, with the
82
+ * `tag-changed` it follows, because the structure a `token` delta says held did
83
+ * not.
84
+ *
81
85
  * Policy may promote or demote from here per project, subject, region, or
82
86
  * component (spec §5). This function is the default, not the answer.
83
87
  */
84
- export declare function bandOf(kind: DeltaKind): Band;
88
+ export declare function bandOf(kind: DeltaKind, { retagged }?: {
89
+ readonly retagged?: boolean;
90
+ }): Band;
85
91
  //# sourceMappingURL=band.d.ts.map
@@ -42,10 +42,14 @@ export function loudestBand(bands) {
42
42
  * there is no rect evidence at all, and the style change stands alone — which is
43
43
  * why JSDOM can decide the token band but only part of geometry.
44
44
  *
45
+ * A style delta on a pair across two tags, `retagged`, is `geometry`, with the
46
+ * `tag-changed` it follows, because the structure a `token` delta says held did
47
+ * not.
48
+ *
45
49
  * Policy may promote or demote from here per project, subject, region, or
46
50
  * component (spec §5). This function is the default, not the answer.
47
51
  */
48
- export function bandOf(kind) {
52
+ export function bandOf(kind, { retagged = false } = {}) {
49
53
  switch (kind) {
50
54
  case 'role-changed':
51
55
  case 'name-changed':
@@ -57,12 +61,13 @@ export function bandOf(kind) {
57
61
  case 'node-added':
58
62
  case 'node-removed':
59
63
  case 'node-moved':
64
+ case 'tag-changed':
60
65
  case 'attribute-changed':
61
66
  case 'rect-changed':
62
67
  return 'geometry';
63
68
  case 'style-changed':
64
69
  case 'token-changed':
65
- return 'token';
70
+ return retagged ? 'geometry' : 'token';
66
71
  case 'raster-residue':
67
72
  return 'texture';
68
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"band.js","sourceRoot":"","sources":["../../src/compare/band.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAwCH,kFAAkF;AAClF,MAAM,CAAC,MAAM,KAAK,GAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;AAE1F;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAAqB;IAC/C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC/B,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;AACzD,CAAC;AAkBD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,MAAM,CAAC,IAAe;IACpC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,cAAc,CAAC;QACpB,KAAK,cAAc,CAAC;QACpB,KAAK,qBAAqB,CAAC;QAC3B,KAAK,eAAe;YAClB,OAAO,MAAM,CAAC;QAChB,KAAK,cAAc;YACjB,OAAO,SAAS,CAAC;QACnB,KAAK,YAAY,CAAC;QAClB,KAAK,cAAc,CAAC;QACpB,KAAK,YAAY,CAAC;QAClB,KAAK,mBAAmB,CAAC;QACzB,KAAK,cAAc;YACjB,OAAO,UAAU,CAAC;QACpB,KAAK,eAAe,CAAC;QACrB,KAAK,eAAe;YAClB,OAAO,OAAO,CAAC;QACjB,KAAK,gBAAgB;YACnB,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC","sourcesContent":["/**\n * Frequency bands (spec §5).\n *\n * Every semantic delta lands in exactly one band, and the band decides how loudly\n * it is reported. The insight the bands encode: change frequency and change\n * importance are inversely correlated. An accessible name almost never moves and\n * is a defect when it does; anti-aliasing moves constantly and never matters.\n *\n * The list is ordered, loudest first, and `loudestBand` is the only thing allowed\n * to collapse a set of them. Three call sites used to hard-code their own\n * `if (bands.includes('geometry'))` ladder, which meant adding a band silently\n * demoted it below everything in every one of them.\n */\n\nexport type Band =\n /**\n * Rarest, and the one a picture cannot hold. A role, an accessible name, or an\n * ARIA state changed — what a screen reader announces moved, whether or not\n * anything was repainted.\n *\n * Split out of `geometry` deliberately. Both are low-frequency, but a box that\n * moved and a control that lost its name are different findings for different\n * readers, and folding the second into the first means the evidence this\n * project collects and no image-comparison tool has is reported under a label\n * that sounds like layout. A project can now write `blocking: ['a11y']` and\n * have an accessibility regression stop a merge on its own terms.\n */\n | 'a11y'\n /** Low frequency. Boxes appear, vanish, move, or resize; content reflows. */\n | 'geometry'\n /** Mid frequency. Style *values* moved while structure held. */\n | 'token'\n /**\n * Text moved and nothing else did.\n *\n * The highest-frequency band that is still a real change: copy edits, counts,\n * dates, translations. Quieter than `geometry` because a reviewer who is told\n * a string changed does not also need to be told a box moved — the reflow is\n * the consequence, and `geometry` outranks it exactly when the tree moved for\n * some other reason too.\n *\n * It is separated from `geometry` because it is the band a message catalogue\n * joins against. A locale run compares the same subject's `content` across two\n * languages, which is a question no baseline image can be asked.\n */\n | 'content'\n /**\n * High frequency. Sub-semantic rendering variance — anti-aliasing, sub-pixel\n * shifts, compression artifacts. Auto-passes; counted, never reviewed.\n */\n | 'texture';\n\n/** Loudest first. `loudestBand` reads this order; nothing else may assume one. */\nexport const BANDS: readonly Band[] = ['a11y', 'geometry', 'token', 'content', 'texture'];\n\n/**\n * The loudest band present, or `null` for an empty set.\n *\n * `null` rather than a default, because \"nothing changed\" and \"something changed\n * at the quietest band\" are different states and a caller that wants to conflate\n * them should have to say so.\n */\nexport function loudestBand(bands: Iterable<Band>): Band | null {\n const present = new Set(bands);\n return BANDS.find((band) => present.has(band)) ?? null;\n}\n\n/** What kind of thing changed, before policy has an opinion about it. */\nexport type DeltaKind =\n | 'node-added'\n | 'node-removed'\n | 'node-moved'\n | 'role-changed'\n | 'name-changed'\n | 'description-changed'\n | 'state-changed'\n | 'text-changed'\n | 'attribute-changed'\n | 'rect-changed'\n | 'style-changed'\n | 'token-changed'\n | 'raster-residue';\n\n/**\n * The band a delta falls in, before policy.\n *\n * `attribute-changed` is `geometry` rather than `a11y` for a reason that is\n * easy to get backwards: **no `aria-*` attribute ever reaches it.**\n * `ATTRIBUTE_ALLOWLIST` drops them all, because each one resolves into a field\n * of its own — `role`, `name`, `description`, `state` — and those are the four\n * kinds banded `a11y`. An `aria-*` delta arriving as `attribute-changed` would\n * mean normalization had stopped resolving it, which is a defect rather than a\n * banding question.\n *\n * Note that `rect-changed` is `geometry` while `style-changed` is `token`, even\n * though a padding change produces both. That is intended: the two are reported\n * at different bands from different evidence, and the attributor folds the rect\n * movement into the style change as collateral. Under a profile without layout\n * there is no rect evidence at all, and the style change stands alone — which is\n * why JSDOM can decide the token band but only part of geometry.\n *\n * Policy may promote or demote from here per project, subject, region, or\n * component (spec §5). This function is the default, not the answer.\n */\nexport function bandOf(kind: DeltaKind): Band {\n switch (kind) {\n case 'role-changed':\n case 'name-changed':\n case 'description-changed':\n case 'state-changed':\n return 'a11y';\n case 'text-changed':\n return 'content';\n case 'node-added':\n case 'node-removed':\n case 'node-moved':\n case 'attribute-changed':\n case 'rect-changed':\n return 'geometry';\n case 'style-changed':\n case 'token-changed':\n return 'token';\n case 'raster-residue':\n return 'texture';\n }\n}\n"]}
1
+ {"version":3,"file":"band.js","sourceRoot":"","sources":["../../src/compare/band.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAwCH,kFAAkF;AAClF,MAAM,CAAC,MAAM,KAAK,GAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;AAE1F;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAAqB;IAC/C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC/B,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;AACzD,CAAC;AAmBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,MAAM,CAAC,IAAe,EAAE,EAAE,QAAQ,GAAG,KAAK,EAAE,GAAoC,EAAE;IAChG,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,cAAc,CAAC;QACpB,KAAK,cAAc,CAAC;QACpB,KAAK,qBAAqB,CAAC;QAC3B,KAAK,eAAe;YAClB,OAAO,MAAM,CAAC;QAChB,KAAK,cAAc;YACjB,OAAO,SAAS,CAAC;QACnB,KAAK,YAAY,CAAC;QAClB,KAAK,cAAc,CAAC;QACpB,KAAK,YAAY,CAAC;QAClB,KAAK,aAAa,CAAC;QACnB,KAAK,mBAAmB,CAAC;QACzB,KAAK,cAAc;YACjB,OAAO,UAAU,CAAC;QACpB,KAAK,eAAe,CAAC;QACrB,KAAK,eAAe;YAClB,OAAO,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;QACzC,KAAK,gBAAgB;YACnB,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC","sourcesContent":["/**\n * Frequency bands (spec §5).\n *\n * Every semantic delta lands in exactly one band, and the band decides how loudly\n * it is reported. The insight the bands encode: change frequency and change\n * importance are inversely correlated. An accessible name almost never moves and\n * is a defect when it does; anti-aliasing moves constantly and never matters.\n *\n * The list is ordered, loudest first, and `loudestBand` is the only thing allowed\n * to collapse a set of them. Three call sites used to hard-code their own\n * `if (bands.includes('geometry'))` ladder, which meant adding a band silently\n * demoted it below everything in every one of them.\n */\n\nexport type Band =\n /**\n * Rarest, and the one a picture cannot hold. A role, an accessible name, or an\n * ARIA state changed — what a screen reader announces moved, whether or not\n * anything was repainted.\n *\n * Split out of `geometry` deliberately. Both are low-frequency, but a box that\n * moved and a control that lost its name are different findings for different\n * readers, and folding the second into the first means the evidence this\n * project collects and no image-comparison tool has is reported under a label\n * that sounds like layout. A project can now write `blocking: ['a11y']` and\n * have an accessibility regression stop a merge on its own terms.\n */\n | 'a11y'\n /** Low frequency. Boxes appear, vanish, move, or resize; content reflows. */\n | 'geometry'\n /** Mid frequency. Style *values* moved while structure held. */\n | 'token'\n /**\n * Text moved and nothing else did.\n *\n * The highest-frequency band that is still a real change: copy edits, counts,\n * dates, translations. Quieter than `geometry` because a reviewer who is told\n * a string changed does not also need to be told a box moved — the reflow is\n * the consequence, and `geometry` outranks it exactly when the tree moved for\n * some other reason too.\n *\n * It is separated from `geometry` because it is the band a message catalogue\n * joins against. A locale run compares the same subject's `content` across two\n * languages, which is a question no baseline image can be asked.\n */\n | 'content'\n /**\n * High frequency. Sub-semantic rendering variance — anti-aliasing, sub-pixel\n * shifts, compression artifacts. Auto-passes; counted, never reviewed.\n */\n | 'texture';\n\n/** Loudest first. `loudestBand` reads this order; nothing else may assume one. */\nexport const BANDS: readonly Band[] = ['a11y', 'geometry', 'token', 'content', 'texture'];\n\n/**\n * The loudest band present, or `null` for an empty set.\n *\n * `null` rather than a default, because \"nothing changed\" and \"something changed\n * at the quietest band\" are different states and a caller that wants to conflate\n * them should have to say so.\n */\nexport function loudestBand(bands: Iterable<Band>): Band | null {\n const present = new Set(bands);\n return BANDS.find((band) => present.has(band)) ?? null;\n}\n\n/** What kind of thing changed, before policy has an opinion about it. */\nexport type DeltaKind =\n | 'node-added'\n | 'node-removed'\n | 'node-moved'\n | 'tag-changed'\n | 'role-changed'\n | 'name-changed'\n | 'description-changed'\n | 'state-changed'\n | 'text-changed'\n | 'attribute-changed'\n | 'rect-changed'\n | 'style-changed'\n | 'token-changed'\n | 'raster-residue';\n\n/**\n * The band a delta falls in, before policy.\n *\n * `attribute-changed` is `geometry` rather than `a11y` for a reason that is\n * easy to get backwards: **no `aria-*` attribute ever reaches it.**\n * `ATTRIBUTE_ALLOWLIST` drops them all, because each one resolves into a field\n * of its own — `role`, `name`, `description`, `state` — and those are the four\n * kinds banded `a11y`. An `aria-*` delta arriving as `attribute-changed` would\n * mean normalization had stopped resolving it, which is a defect rather than a\n * banding question.\n *\n * Note that `rect-changed` is `geometry` while `style-changed` is `token`, even\n * though a padding change produces both. That is intended: the two are reported\n * at different bands from different evidence, and the attributor folds the rect\n * movement into the style change as collateral. Under a profile without layout\n * there is no rect evidence at all, and the style change stands alone — which is\n * why JSDOM can decide the token band but only part of geometry.\n *\n * A style delta on a pair across two tags, `retagged`, is `geometry`, with the\n * `tag-changed` it follows, because the structure a `token` delta says held did\n * not.\n *\n * Policy may promote or demote from here per project, subject, region, or\n * component (spec §5). This function is the default, not the answer.\n */\nexport function bandOf(kind: DeltaKind, { retagged = false }: { readonly retagged?: boolean } = {}): Band {\n switch (kind) {\n case 'role-changed':\n case 'name-changed':\n case 'description-changed':\n case 'state-changed':\n return 'a11y';\n case 'text-changed':\n return 'content';\n case 'node-added':\n case 'node-removed':\n case 'node-moved':\n case 'tag-changed':\n case 'attribute-changed':\n case 'rect-changed':\n return 'geometry';\n case 'style-changed':\n case 'token-changed':\n return retagged ? 'geometry' : 'token';\n case 'raster-residue':\n return 'texture';\n }\n}\n"]}
@@ -66,10 +66,12 @@ export function attribute(deltas, matching, environmentDeltas) {
66
66
  // change even though every node that moved is a `Chip`.
67
67
  //
68
68
  // Appearing and disappearing are deliberately *not* treated this way. A
69
- // component that swaps its own output — `Toggle` rendering a `<div>` where
70
- // it used to render an `<input>` — produces an added and a removed node
71
- // whose creator is `Toggle` itself, and crediting whoever placed `<Toggle>`
72
- // would blame `TodoItem` for a change it did not make.
69
+ // component that swaps its own output produces nodes whose creator is that
70
+ // component itself, and crediting whoever placed it would blame its parent
71
+ // for a change it did not make. One swap — `Toggle` rendering a `<div>`
72
+ // where it used to render an `<input>` — is a matched pair and arrives
73
+ // here as `tag-changed` (see `match.ts`); two swaps by one author stay an
74
+ // added and a removed node each, and the same rule names `Toggle` for both.
73
75
  //
74
76
  // Known cost: when a component element is added or removed wholesale, this
75
77
  // names the component that appeared rather than the one that decided to
@@ -1 +1 @@
1
- {"version":3,"file":"attribution.js","sourceRoot":"","sources":["../../../src/compare/diff/attribution.ts"],"names":[],"mappings":"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","sourcesContent":["import { loudestBand, type Band } from '../band.js';\nimport { aggregateImpact } from '../impact.js';\nimport type { EnvironmentDelta } from '../../format/environment.js';\nimport type { OwnerFrame } from '../../format/provenance.js';\nimport { impactTag, type Delta, type Root, type RootKind } from './delta.js';\nimport type { Matching } from './match.js';\n\n/**\n * Grouping deltas into explanations — the docket's entire value proposition.\n *\n * Separated from `index.ts` because it is the half of diffing that has nothing\n * to do with observation: it takes deltas that already exist and decides which\n * *one* sentence covers them. The precedence rules, the metric-delta folding and\n * the boundary walk are all in service of the same claim — report the largest\n * explanation once rather than the smallest one many times — and they only make\n * sense read together.\n */\n\n/**\n * Group deltas into explanations (spec §6.2).\n *\n * Precedence is fixed and deliberate: environment, then token, then props, then\n * component. Each level explains more subjects than the one below it, and the\n * docket's value is entirely in reporting the *largest* explanation once rather\n * than the smallest one many times.\n */\nexport function attribute(\n deltas: readonly Delta[],\n matching: Matching,\n environmentDeltas: readonly EnvironmentDelta[],\n): readonly Root[] {\n const groups = new Map<string, { kind: RootKind; label: string; cause?: string; deltas: Delta[] }>();\n\n const add = (id: string, kind: RootKind, label: string, delta: Delta, cause?: string): void => {\n const existing = groups.get(id);\n if (existing) existing.deltas.push(delta);\n else groups.set(id, { kind, label, deltas: [delta], ...(cause !== undefined ? { cause } : {}) });\n };\n\n // A changed environment explains everything under it, so it is collapsed to a\n // single root — the one-action re-baselining case of spec §7.3.\n if (environmentDeltas.length > 0) {\n for (const environmentDelta of environmentDeltas) {\n groups.set(`env:${environmentDelta.field}`, {\n kind: 'environment',\n label: `${environmentDelta.field} ${environmentDelta.from} → ${environmentDelta.to}`,\n deltas: [],\n });\n }\n }\n\n const propsBefore = ownerDigests(matching, 'before');\n const propsAfter = ownerDigests(matching, 'after');\n\n const classify = (delta: Delta): Group | null => {\n if (delta.token) return { id: `token:${delta.token}`, kind: 'token', label: delta.token };\n\n const owners = delta.owners;\n if (owners === undefined || owners.length === 0) return null;\n\n const boundary = changedBoundary(owners, propsBefore, propsAfter);\n\n if (boundary === null) {\n // Every incoming props digest held, so the change originated inside a\n // component rather than arriving from outside.\n //\n // *Which* component depends on what kind of change it is. A style change\n // belongs to the component the styled node sits in. A node appearing,\n // vanishing, or moving belongs to whoever *decided it is there* — the\n // component whose JSX created the element, not the component the element\n // is. When a filter list reorders, the nodes that moved are `Chip`s\n // enclosed by a `Stack`, and naming either reports the thing that was\n // rearranged instead of the code that rearranged it.\n const owner = owners[0]!;\n\n // A node that *moved* is the only case where the responsible component is\n // not the one the node belongs to. Ordering is decided by whoever wrote the\n // JSX that placed the element, so a reordered filter list is `TodoFooter`'s\n // change even though every node that moved is a `Chip`.\n //\n // Appearing and disappearing are deliberately *not* treated this way. A\n // component that swaps its own output — `Toggle` rendering a `<div>` where\n // it used to render an `<input>` — produces an added and a removed node\n // whose creator is `Toggle` itself, and crediting whoever placed `<Toggle>`\n // would blame `TodoItem` for a change it did not make.\n //\n // Known cost: when a component element is added or removed wholesale, this\n // names the component that appeared rather than the one that decided to\n // render it. Distinguishing those needs to know whether the component still\n // exists on the other side, which is a question about the change set rather\n // than about the delta.\n const name = (delta.kind === 'node-moved' ? owner.createdBy : undefined) ?? owner.name;\n\n return { id: `component:${name}`, kind: 'component', label: name, cause: name };\n }\n\n // Props moved at a boundary, so the change arrived from outside. The root is\n // the provider — the component that passes props across that boundary.\n const changed = owners[boundary]!;\n const provider = owners[boundary + 1];\n return {\n id: `prop:${provider?.name ?? '?'}>${changed.name}`,\n kind: 'prop',\n label: provider ? `${provider.name} → ${changed.name}` : changed.name,\n // The *provider*, not the component the delta landed in. This is the whole\n // point of a `prop` root: the edit is upstream, and the report has to send\n // a reviewer there.\n //\n // Set **only when a provider exists**. With none, the changed boundary is\n // the outermost frame in the chain: the props arrived from outside the\n // subject entirely, and no component inside it is responsible. Naming the\n // boundary there would send a reviewer to a component whose source is also\n // unchanged, which is the same failure one level out. The fallback in\n // `componentsOf` — the innermost owner, where the change landed — is what\n // both corpora ask for in that case, and the entry's own label still says\n // whose props moved.\n ...(provider !== undefined ? { cause: provider.name } : {}),\n };\n };\n\n // Metric deltas are held back until every cause has a group, then attached to\n // the nearest one. Without this a profile with layout reports a root per\n // component whose box happened to resize — the change is not lost, but \"one\n // root plus counted collateral\" becomes \"one root per affected component\",\n // which is the report spec §6.2 exists to prevent.\n const causes = deltas.filter((delta) => !isMetric(delta)).map((delta) => ({ delta, group: classify(delta) }));\n\n for (const { delta, group } of causes) {\n if (group === null) add('unattributed', 'unattributed', 'no owner chain', delta);\n else add(group.id, group.kind, group.label, delta, group.cause);\n }\n\n for (const delta of deltas) {\n if (!isMetric(delta)) continue;\n\n const target = nearestCause(delta, causes);\n\n if (target !== undefined) {\n add(target.id, target.kind, target.label, delta, target.cause);\n continue;\n }\n\n // No cause anywhere: the box moved and nothing explains it. That is a real\n // finding — most often a change outside the subject reaching in — and it\n // keeps its own root rather than being attached to an unrelated one.\n const own = classify(delta);\n if (own === null) add('unattributed', 'unattributed', 'no owner chain', delta);\n else add(own.id, own.kind, own.label, delta, own.cause);\n }\n\n return [...groups.entries()].map(([id, group]) => ({\n id,\n kind: group.kind,\n label: group.label,\n ...(group.cause !== undefined ? { cause: group.cause } : {}),\n band: dominantBand(group.deltas),\n impact: aggregateImpact(group.deltas.map(impactTag)),\n deltas: group.deltas,\n }));\n}\n\ninterface Group {\n readonly id: string;\n readonly kind: RootKind;\n readonly label: string;\n /** The component responsible. See `Root.cause`. */\n readonly cause?: string;\n}\n\n/**\n * Computed properties that are functions of the used box rather than of any\n * declaration.\n *\n * A layout engine reports these for every element whether or not a stylesheet\n * mentioned them, so they move whenever anything inside or above the node\n * changes size. Treating them as causes is what turned one token edit into five\n * docket entries under `chromium` — `band.ts` already says the attributor must\n * fold rect movement into the style change as collateral, and these are the\n * `style-changed` half of the same evidence.\n *\n * Note the cost of the simplification: a genuine `width: 100px → 200px` edit is\n * folded too, whenever the same node carries another delta to fold into. That\n * loses no delta and moves no verdict — the change is still reported, under a\n * root that is a strictly better description of the cause.\n */\nconst USED_VALUE_PROPERTIES: ReadonlySet<string> = new Set([\n 'width', 'height', 'inline-size', 'block-size',\n 'transform-origin', 'perspective-origin',\n]);\n\n/** Evidence that a box moved, as opposed to evidence of why. */\nfunction isMetric(delta: Delta): boolean {\n if (delta.kind === 'rect-changed') return true;\n return delta.kind === 'style-changed' && USED_VALUE_PROPERTIES.has(delta.property ?? '');\n}\n\n/** Paths are `/`-joined child indices, so containment is a prefix test. */\nfunction isDescendant(candidate: string, ancestor: string): boolean {\n return candidate.startsWith(`${ancestor}/`);\n}\n\n/**\n * The cause a metric delta belongs to: same node first, then the nearest change\n * *inside* it, then the nearest change above it.\n *\n * Inside before above, because a box that grew did so because of its contents\n * far more often than because of its container — and when a container really is\n * the cause, its own metric delta folds upward on the same rule, so the two\n * meet at the same root either way.\n */\nfunction nearestCause(\n delta: Delta,\n causes: readonly { readonly delta: Delta; readonly group: Group | null }[],\n): Group | undefined {\n const grouped = causes.filter((cause) => cause.group !== null);\n\n const here = grouped.find((cause) => cause.delta.path === delta.path);\n if (here) return here.group ?? undefined;\n\n const inside = grouped\n .filter((cause) => isDescendant(cause.delta.path, delta.path))\n .sort((a, b) => a.delta.path.length - b.delta.path.length)[0];\n if (inside) return inside.group ?? undefined;\n\n const above = grouped\n .filter((cause) => isDescendant(delta.path, cause.delta.path))\n .sort((a, b) => b.delta.path.length - a.delta.path.length)[0];\n\n return above?.group ?? undefined;\n}\n\n/**\n * The outermost owner whose incoming props changed, or `null` if none did.\n *\n * Outermost rather than innermost: if `CheckoutPage` passes a new prop to\n * `Header`, which passes it to `Button`, all three boundaries moved, and the\n * useful root is the one furthest up — reporting `Button` would name the\n * messenger.\n */\nfunction changedBoundary(\n owners: readonly OwnerFrame[],\n before: ReadonlyMap<string, string>,\n after: ReadonlyMap<string, string>,\n): number | null {\n let outermost: number | null = null;\n\n for (let index = 0; index < owners.length; index += 1) {\n const owner = owners[index]!;\n const digestBefore = before.get(owner.name);\n // An owner absent from the baseline is new structure, not a prop change;\n // the structural delta covers it and inventing a prop root would double-count.\n if (digestBefore === undefined) continue;\n if (digestBefore !== after.get(owner.name)) outermost = index;\n }\n\n return outermost;\n}\n\nfunction ownerDigests(\n matching: Matching,\n side: 'before' | 'after',\n): ReadonlyMap<string, string> {\n const digests = new Map<string, string>();\n\n for (const [before, after] of matching.pairs) {\n const node = side === 'before' ? before : after;\n for (const owner of node.provenance?.owners ?? []) {\n // First occurrence wins: a component rendered many times with different\n // props is not the same boundary, and this map answers the coarser\n // question of whether *anything* at that boundary moved.\n if (!digests.has(owner.name)) digests.set(owner.name, owner.propsDigest);\n }\n }\n\n return digests;\n}\n\n/**\n * Loudest band present. A group containing one `a11y` delta is an `a11y` root,\n * whatever else moved alongside it.\n *\n * `texture` is the fallback for the empty case rather than a claim: a root is\n * only built from deltas, so the set is never actually empty here.\n */\nfunction dominantBand(deltas: readonly Delta[]): Band {\n return loudestBand(deltas.map((delta) => delta.band)) ?? 'texture';\n}\n"]}
1
+ {"version":3,"file":"attribution.js","sourceRoot":"","sources":["../../../src/compare/diff/attribution.ts"],"names":[],"mappings":"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","sourcesContent":["import { loudestBand, type Band } from '../band.js';\nimport { aggregateImpact } from '../impact.js';\nimport type { EnvironmentDelta } from '../../format/environment.js';\nimport type { OwnerFrame } from '../../format/provenance.js';\nimport { impactTag, type Delta, type Root, type RootKind } from './delta.js';\nimport type { Matching } from './match.js';\n\n/**\n * Grouping deltas into explanations — the docket's entire value proposition.\n *\n * Separated from `index.ts` because it is the half of diffing that has nothing\n * to do with observation: it takes deltas that already exist and decides which\n * *one* sentence covers them. The precedence rules, the metric-delta folding and\n * the boundary walk are all in service of the same claim — report the largest\n * explanation once rather than the smallest one many times — and they only make\n * sense read together.\n */\n\n/**\n * Group deltas into explanations (spec §6.2).\n *\n * Precedence is fixed and deliberate: environment, then token, then props, then\n * component. Each level explains more subjects than the one below it, and the\n * docket's value is entirely in reporting the *largest* explanation once rather\n * than the smallest one many times.\n */\nexport function attribute(\n deltas: readonly Delta[],\n matching: Matching,\n environmentDeltas: readonly EnvironmentDelta[],\n): readonly Root[] {\n const groups = new Map<string, { kind: RootKind; label: string; cause?: string; deltas: Delta[] }>();\n\n const add = (id: string, kind: RootKind, label: string, delta: Delta, cause?: string): void => {\n const existing = groups.get(id);\n if (existing) existing.deltas.push(delta);\n else groups.set(id, { kind, label, deltas: [delta], ...(cause !== undefined ? { cause } : {}) });\n };\n\n // A changed environment explains everything under it, so it is collapsed to a\n // single root — the one-action re-baselining case of spec §7.3.\n if (environmentDeltas.length > 0) {\n for (const environmentDelta of environmentDeltas) {\n groups.set(`env:${environmentDelta.field}`, {\n kind: 'environment',\n label: `${environmentDelta.field} ${environmentDelta.from} → ${environmentDelta.to}`,\n deltas: [],\n });\n }\n }\n\n const propsBefore = ownerDigests(matching, 'before');\n const propsAfter = ownerDigests(matching, 'after');\n\n const classify = (delta: Delta): Group | null => {\n if (delta.token) return { id: `token:${delta.token}`, kind: 'token', label: delta.token };\n\n const owners = delta.owners;\n if (owners === undefined || owners.length === 0) return null;\n\n const boundary = changedBoundary(owners, propsBefore, propsAfter);\n\n if (boundary === null) {\n // Every incoming props digest held, so the change originated inside a\n // component rather than arriving from outside.\n //\n // *Which* component depends on what kind of change it is. A style change\n // belongs to the component the styled node sits in. A node appearing,\n // vanishing, or moving belongs to whoever *decided it is there* — the\n // component whose JSX created the element, not the component the element\n // is. When a filter list reorders, the nodes that moved are `Chip`s\n // enclosed by a `Stack`, and naming either reports the thing that was\n // rearranged instead of the code that rearranged it.\n const owner = owners[0]!;\n\n // A node that *moved* is the only case where the responsible component is\n // not the one the node belongs to. Ordering is decided by whoever wrote the\n // JSX that placed the element, so a reordered filter list is `TodoFooter`'s\n // change even though every node that moved is a `Chip`.\n //\n // Appearing and disappearing are deliberately *not* treated this way. A\n // component that swaps its own output produces nodes whose creator is that\n // component itself, and crediting whoever placed it would blame its parent\n // for a change it did not make. One swap — `Toggle` rendering a `<div>`\n // where it used to render an `<input>` — is a matched pair and arrives\n // here as `tag-changed` (see `match.ts`); two swaps by one author stay an\n // added and a removed node each, and the same rule names `Toggle` for both.\n //\n // Known cost: when a component element is added or removed wholesale, this\n // names the component that appeared rather than the one that decided to\n // render it. Distinguishing those needs to know whether the component still\n // exists on the other side, which is a question about the change set rather\n // than about the delta.\n const name = (delta.kind === 'node-moved' ? owner.createdBy : undefined) ?? owner.name;\n\n return { id: `component:${name}`, kind: 'component', label: name, cause: name };\n }\n\n // Props moved at a boundary, so the change arrived from outside. The root is\n // the provider — the component that passes props across that boundary.\n const changed = owners[boundary]!;\n const provider = owners[boundary + 1];\n return {\n id: `prop:${provider?.name ?? '?'}>${changed.name}`,\n kind: 'prop',\n label: provider ? `${provider.name} → ${changed.name}` : changed.name,\n // The *provider*, not the component the delta landed in. This is the whole\n // point of a `prop` root: the edit is upstream, and the report has to send\n // a reviewer there.\n //\n // Set **only when a provider exists**. With none, the changed boundary is\n // the outermost frame in the chain: the props arrived from outside the\n // subject entirely, and no component inside it is responsible. Naming the\n // boundary there would send a reviewer to a component whose source is also\n // unchanged, which is the same failure one level out. The fallback in\n // `componentsOf` — the innermost owner, where the change landed — is what\n // both corpora ask for in that case, and the entry's own label still says\n // whose props moved.\n ...(provider !== undefined ? { cause: provider.name } : {}),\n };\n };\n\n // Metric deltas are held back until every cause has a group, then attached to\n // the nearest one. Without this a profile with layout reports a root per\n // component whose box happened to resize — the change is not lost, but \"one\n // root plus counted collateral\" becomes \"one root per affected component\",\n // which is the report spec §6.2 exists to prevent.\n const causes = deltas.filter((delta) => !isMetric(delta)).map((delta) => ({ delta, group: classify(delta) }));\n\n for (const { delta, group } of causes) {\n if (group === null) add('unattributed', 'unattributed', 'no owner chain', delta);\n else add(group.id, group.kind, group.label, delta, group.cause);\n }\n\n for (const delta of deltas) {\n if (!isMetric(delta)) continue;\n\n const target = nearestCause(delta, causes);\n\n if (target !== undefined) {\n add(target.id, target.kind, target.label, delta, target.cause);\n continue;\n }\n\n // No cause anywhere: the box moved and nothing explains it. That is a real\n // finding — most often a change outside the subject reaching in — and it\n // keeps its own root rather than being attached to an unrelated one.\n const own = classify(delta);\n if (own === null) add('unattributed', 'unattributed', 'no owner chain', delta);\n else add(own.id, own.kind, own.label, delta, own.cause);\n }\n\n return [...groups.entries()].map(([id, group]) => ({\n id,\n kind: group.kind,\n label: group.label,\n ...(group.cause !== undefined ? { cause: group.cause } : {}),\n band: dominantBand(group.deltas),\n impact: aggregateImpact(group.deltas.map(impactTag)),\n deltas: group.deltas,\n }));\n}\n\ninterface Group {\n readonly id: string;\n readonly kind: RootKind;\n readonly label: string;\n /** The component responsible. See `Root.cause`. */\n readonly cause?: string;\n}\n\n/**\n * Computed properties that are functions of the used box rather than of any\n * declaration.\n *\n * A layout engine reports these for every element whether or not a stylesheet\n * mentioned them, so they move whenever anything inside or above the node\n * changes size. Treating them as causes is what turned one token edit into five\n * docket entries under `chromium` — `band.ts` already says the attributor must\n * fold rect movement into the style change as collateral, and these are the\n * `style-changed` half of the same evidence.\n *\n * Note the cost of the simplification: a genuine `width: 100px → 200px` edit is\n * folded too, whenever the same node carries another delta to fold into. That\n * loses no delta and moves no verdict — the change is still reported, under a\n * root that is a strictly better description of the cause.\n */\nconst USED_VALUE_PROPERTIES: ReadonlySet<string> = new Set([\n 'width', 'height', 'inline-size', 'block-size',\n 'transform-origin', 'perspective-origin',\n]);\n\n/** Evidence that a box moved, as opposed to evidence of why. */\nfunction isMetric(delta: Delta): boolean {\n if (delta.kind === 'rect-changed') return true;\n return delta.kind === 'style-changed' && USED_VALUE_PROPERTIES.has(delta.property ?? '');\n}\n\n/** Paths are `/`-joined child indices, so containment is a prefix test. */\nfunction isDescendant(candidate: string, ancestor: string): boolean {\n return candidate.startsWith(`${ancestor}/`);\n}\n\n/**\n * The cause a metric delta belongs to: same node first, then the nearest change\n * *inside* it, then the nearest change above it.\n *\n * Inside before above, because a box that grew did so because of its contents\n * far more often than because of its container — and when a container really is\n * the cause, its own metric delta folds upward on the same rule, so the two\n * meet at the same root either way.\n */\nfunction nearestCause(\n delta: Delta,\n causes: readonly { readonly delta: Delta; readonly group: Group | null }[],\n): Group | undefined {\n const grouped = causes.filter((cause) => cause.group !== null);\n\n const here = grouped.find((cause) => cause.delta.path === delta.path);\n if (here) return here.group ?? undefined;\n\n const inside = grouped\n .filter((cause) => isDescendant(cause.delta.path, delta.path))\n .sort((a, b) => a.delta.path.length - b.delta.path.length)[0];\n if (inside) return inside.group ?? undefined;\n\n const above = grouped\n .filter((cause) => isDescendant(delta.path, cause.delta.path))\n .sort((a, b) => b.delta.path.length - a.delta.path.length)[0];\n\n return above?.group ?? undefined;\n}\n\n/**\n * The outermost owner whose incoming props changed, or `null` if none did.\n *\n * Outermost rather than innermost: if `CheckoutPage` passes a new prop to\n * `Header`, which passes it to `Button`, all three boundaries moved, and the\n * useful root is the one furthest up — reporting `Button` would name the\n * messenger.\n */\nfunction changedBoundary(\n owners: readonly OwnerFrame[],\n before: ReadonlyMap<string, string>,\n after: ReadonlyMap<string, string>,\n): number | null {\n let outermost: number | null = null;\n\n for (let index = 0; index < owners.length; index += 1) {\n const owner = owners[index]!;\n const digestBefore = before.get(owner.name);\n // An owner absent from the baseline is new structure, not a prop change;\n // the structural delta covers it and inventing a prop root would double-count.\n if (digestBefore === undefined) continue;\n if (digestBefore !== after.get(owner.name)) outermost = index;\n }\n\n return outermost;\n}\n\nfunction ownerDigests(\n matching: Matching,\n side: 'before' | 'after',\n): ReadonlyMap<string, string> {\n const digests = new Map<string, string>();\n\n for (const [before, after] of matching.pairs) {\n const node = side === 'before' ? before : after;\n for (const owner of node.provenance?.owners ?? []) {\n // First occurrence wins: a component rendered many times with different\n // props is not the same boundary, and this map answers the coarser\n // question of whether *anything* at that boundary moved.\n if (!digests.has(owner.name)) digests.set(owner.name, owner.propsDigest);\n }\n }\n\n return digests;\n}\n\n/**\n * Loudest band present. A group containing one `a11y` delta is an `a11y` root,\n * whatever else moved alongside it.\n *\n * `texture` is the fallback for the empty case rather than a claim: a root is\n * only built from deltas, so the set is never actually empty here.\n */\nfunction dominantBand(deltas: readonly Delta[]): Band {\n return loudestBand(deltas.map((delta) => delta.band)) ?? 'texture';\n}\n"]}
@@ -32,13 +32,21 @@ function describe(node) {
32
32
  export function compareNodes(before, after, deltas, hasLayout) {
33
33
  const owners = after.provenance?.owners;
34
34
  const createdBy = after.provenance?.createdBy;
35
+ const retagged = before.tag !== after.tag;
35
36
  const base = (kind) => ({
36
37
  kind,
37
- band: bandOf(kind),
38
+ band: bandOf(kind, { retagged }),
38
39
  path: after.path,
39
40
  ...(owners ? { owners } : {}),
40
41
  ...(createdBy ? { createdBy } : {}),
41
42
  });
43
+ // Two tags pair as the two roots, under a declared alias, under one role and
44
+ // name, or as the one replacement its author recorded (see `match.ts`). The tag is said first: the
45
+ // role, the attributes and the box that follow are what the new element is,
46
+ // not edits to the old one.
47
+ if (retagged) {
48
+ deltas.push({ ...base('tag-changed'), from: before.tag, to: after.tag });
49
+ }
42
50
  if (before.role !== after.role) {
43
51
  deltas.push({ ...base('role-changed'), from: before.role, to: after.role });
44
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"compare-nodes.js","sourceRoot":"","sources":["../../../src/compare/diff/compare-nodes.ts"],"names":[],"mappings":"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","sourcesContent":["import { bandOf, type DeltaKind } from '../band.js';\nimport { impactOf } from '../impact.js';\nimport type { SemanticNode } from '../../format/snapshot.js';\nimport type { Delta } from './delta.js';\n\n/**\n * Delta production for a single node, or a single matched pair of nodes.\n *\n * Separated from `index.ts` because everything here answers a strictly local\n * question — given two nodes already known to be the same node, which fields\n * moved — and knows nothing about trees, roots, or attribution. The token\n * reasoning lives here rather than with the attributor for the same reason: the\n * evidence that decides whether a property change is collateral of a token edit\n * is the pair of nodes, and nothing further out has it.\n */\n\nexport function wholeNode(\n kind: 'node-added' | 'node-removed' | 'node-moved',\n node: SemanticNode,\n): Delta {\n return {\n kind,\n band: bandOf(kind),\n path: node.path,\n to: describe(node),\n ...(node.provenance ? { owners: node.provenance.owners } : {}),\n ...(node.provenance?.createdBy ? { createdBy: node.provenance.createdBy } : {}),\n };\n}\n\nfunction describe(node: SemanticNode): string {\n if (node.role !== undefined && node.name !== undefined) return `${node.role} \"${node.name}\"`;\n if (node.role !== undefined) return node.role;\n if (node.text !== undefined) return `${node.tag} \"${node.text}\"`;\n return node.tag;\n}\n\nexport function compareNodes(\n before: SemanticNode,\n after: SemanticNode,\n deltas: Delta[],\n hasLayout: boolean,\n): void {\n const owners = after.provenance?.owners;\n const createdBy = after.provenance?.createdBy;\n const base = (kind: DeltaKind) => ({\n kind,\n band: bandOf(kind),\n path: after.path,\n ...(owners ? { owners } : {}),\n ...(createdBy ? { createdBy } : {}),\n });\n\n if (before.role !== after.role) {\n deltas.push({ ...base('role-changed'), from: before.role, to: after.role });\n }\n if (before.name !== after.name) {\n deltas.push({ ...base('name-changed'), from: before.name, to: after.name });\n }\n if (before.description !== after.description) {\n deltas.push({\n ...base('description-changed'),\n from: before.description,\n to: after.description,\n });\n }\n if (before.text !== after.text) {\n deltas.push({ ...base('text-changed'), from: before.text, to: after.text });\n }\n\n for (const property of unionKeys(before.state, after.state)) {\n const from = before.state?.[property];\n const to = after.state?.[property];\n if (from !== to) {\n deltas.push({ ...base('state-changed'), property, from: str(from), to: str(to) });\n }\n }\n\n for (const property of unionKeys(before.attributes, after.attributes)) {\n const from = before.attributes[property];\n const to = after.attributes[property];\n if (from !== to) {\n deltas.push({ ...base('attribute-changed'), property, from, to });\n }\n }\n\n let reflowCause: Delta | null = null;\n\n for (const property of unionKeys(before.style, after.style)) {\n const from = before.style[property];\n const to = after.style[property];\n if (from === to) continue;\n\n // A token whose own value moved makes this delta collateral rather than a\n // root. Recorded here so attribution does not have to re-derive it.\n const token = changedTokenFor(before, after, property);\n const impact = impactOf(property);\n\n const delta: Delta = {\n ...base('style-changed'),\n property,\n from,\n to,\n impact,\n ...(token ? { token } : {}),\n };\n\n deltas.push(delta);\n if (impact === 'layout' && reflowCause === null) reflowCause = delta;\n }\n\n // Which token a property resolves *through* is part of the render hash, so a\n // change in it moves the hash — but under a profile that cannot resolve custom\n // properties both resolved values are the same empty string and the loop above\n // sees nothing. The result was a non-identical diff carrying zero deltas and\n // zero roots: a correct verdict with an empty docket, which tells a reviewer\n // that something changed and then refuses to say what. Found by scoring\n // `prop-size/button` under `jsdom`, which only became scorable with ADR-0008.\n //\n // Skipped when the resolved value moved too: the `style-changed` delta above\n // already names that property, and reporting both splits one cause in two.\n for (const property of unionKeys(before.styleTokens, after.styleTokens)) {\n const from = before.styleTokens?.[property];\n const to = after.styleTokens?.[property];\n if (from === to) continue;\n if (before.style[property] !== after.style[property]) continue;\n\n deltas.push({ ...base('token-changed'), property, from, to });\n }\n\n if (hasLayout && !sameRect(before, after)) {\n // A rect that moved because this node's own padding changed is the same\n // finding observed twice. Folding it under its cause — and inheriting that\n // cause's token — keeps one edit as one docket entry instead of splitting it\n // into a `token` root and an unrelated-looking `geometry` one.\n //\n // A rect that moved with *no* layout-impact change here is different and\n // stays independent: something upstream reflowed and pushed this node, which\n // is exactly the propagation worth surfacing.\n deltas.push({\n ...base('rect-changed'),\n ...(before.rect ? { rectFrom: before.rect } : {}),\n ...(after.rect ? { rectTo: after.rect } : {}),\n ...(reflowCause\n ? {\n derivedFrom: `${reflowCause.path}:${reflowCause.property ?? ''}`,\n ...(reflowCause.token ? { token: reflowCause.token } : {}),\n }\n : {}),\n });\n }\n}\n\n/**\n * The token that explains *this property's* change, if one does.\n *\n * Two conditions, and both are load-bearing. The property must actually resolve\n * through the token — a node whose `color` comes from a token and whose padding\n * comes from a literal is collateral of a token edit only in its colour. And the\n * token's own value must have moved: a node that merely mentions\n * `--color-primary` while that token held is not collateral of anything, its own\n * rule changed, and it is a root.\n *\n * Dropping either check splits one docket entry into several. Ignoring the\n * property meant a rule that overrode a token-driven value got attributed to the\n * token it had just stopped using.\n */\nfunction changedTokenFor(\n before: SemanticNode,\n after: SemanticNode,\n property: string,\n): string | undefined {\n const token = before.styleTokens?.[property];\n\n // The *same* token must drive the property on both sides. A property that\n // stopped resolving through a token did so because some rule started winning\n // instead — that rule is the root, and blaming the abandoned token would name\n // the thing that did not change.\n // Properties whose initial value is `currentColor` follow `color` without ever\n // naming it: no rule declares `outline-color`, so it resolves through no token,\n // yet a token-driven `color` change moves it. Under a profile with computed\n // style that produced a second root per Button — \"the accent token moved\" and\n // \"Button changed\" — for one edit. Recognised by the value matching `color` on\n // both sides, so a node that genuinely declares its own outline colour is\n // unaffected.\n if (token === undefined && FOLLOWS_CURRENT_COLOR.has(property)) {\n const from = before.style[property];\n const to = after.style[property];\n if (from !== undefined && to !== undefined && from === before.style['color'] && to === after.style['color']) {\n return changedTokenFor(before, after, 'color');\n }\n return undefined;\n }\n\n if (token === undefined || after.styleTokens?.[property] !== token) return undefined;\n\n // Undefined on one side counts as a change: a theme override introducing a\n // token that previously had no value is exactly the case the token band exists\n // to collapse into one root.\n if (before.tokens?.[token] !== after.tokens?.[token]) return token;\n\n // The named token held — and may never have had a value at all.\n // `var(--ks-card-radius, var(--va-radius-md))` is the ordinary shape of a\n // component token with a system fallback: `styleTokens` names the outer one,\n // because that is what the author wrote, and when it is undefined the value\n // came from the fallback. Comparing the outer name's value then compares\n // `undefined` with `undefined`, concludes the token held, and reports an edit\n // to the radius *scale* as `component:Card` — \"Card changed internally\", once\n // per consuming component — instead of one token root with counted collateral.\n // That is the product's headline claim failing on the most common\n // design-system shape there is, and it survived because the corpus asserted\n // the root *count*, which is one either way.\n return soleMovedToken(before, after);\n}\n\n/**\n * The one token this node resolved through whose value moved, if there is\n * exactly one.\n *\n * The fallback for a chain whose outer name carries no value. Deliberately\n * refuses to answer when two moved: \"the named token held and several others\n * changed\" is genuinely ambiguous, and picking one would put a confident wrong\n * name in front of a reviewer. Ambiguity falls through to the component root,\n * which is coarser and true.\n */\nfunction soleMovedToken(before: SemanticNode, after: SemanticNode): string | undefined {\n const moved = unionKeys(before.tokens, after.tokens).filter(\n (name) => before.tokens?.[name] !== after.tokens?.[name],\n );\n\n return moved.length === 1 ? moved[0] : undefined;\n}\n\n/**\n * Properties whose initial value is `currentColor`.\n *\n * `color` itself is excluded, or the lookup would recurse.\n */\nconst FOLLOWS_CURRENT_COLOR: ReadonlySet<string> = new Set([\n 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',\n 'outline-color', 'text-decoration-color', 'text-emphasis-color', 'column-rule-color',\n 'caret-color',\n]);\n\nfunction sameRect(before: SemanticNode, after: SemanticNode): boolean {\n const a = before.rect;\n const b = after.rect;\n if (a === undefined || b === undefined) return a === b;\n return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;\n}\n\nfunction unionKeys(\n a: Readonly<Record<string, unknown>> | undefined,\n b: Readonly<Record<string, unknown>> | undefined,\n): readonly string[] {\n return [...new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})])].sort();\n}\n\nfunction str(value: unknown): string | undefined {\n return value === undefined ? undefined : String(value);\n}\n"]}
1
+ {"version":3,"file":"compare-nodes.js","sourceRoot":"","sources":["../../../src/compare/diff/compare-nodes.ts"],"names":[],"mappings":"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","sourcesContent":["import { bandOf, type DeltaKind } from '../band.js';\nimport { impactOf } from '../impact.js';\nimport type { SemanticNode } from '../../format/snapshot.js';\nimport type { Delta } from './delta.js';\n\n/**\n * Delta production for a single node, or a single matched pair of nodes.\n *\n * Separated from `index.ts` because everything here answers a strictly local\n * question — given two nodes already known to be the same node, which fields\n * moved — and knows nothing about trees, roots, or attribution. The token\n * reasoning lives here rather than with the attributor for the same reason: the\n * evidence that decides whether a property change is collateral of a token edit\n * is the pair of nodes, and nothing further out has it.\n */\n\nexport function wholeNode(\n kind: 'node-added' | 'node-removed' | 'node-moved',\n node: SemanticNode,\n): Delta {\n return {\n kind,\n band: bandOf(kind),\n path: node.path,\n to: describe(node),\n ...(node.provenance ? { owners: node.provenance.owners } : {}),\n ...(node.provenance?.createdBy ? { createdBy: node.provenance.createdBy } : {}),\n };\n}\n\nfunction describe(node: SemanticNode): string {\n if (node.role !== undefined && node.name !== undefined) return `${node.role} \"${node.name}\"`;\n if (node.role !== undefined) return node.role;\n if (node.text !== undefined) return `${node.tag} \"${node.text}\"`;\n return node.tag;\n}\n\nexport function compareNodes(\n before: SemanticNode,\n after: SemanticNode,\n deltas: Delta[],\n hasLayout: boolean,\n): void {\n const owners = after.provenance?.owners;\n const createdBy = after.provenance?.createdBy;\n const retagged = before.tag !== after.tag;\n const base = (kind: DeltaKind) => ({\n kind,\n band: bandOf(kind, { retagged }),\n path: after.path,\n ...(owners ? { owners } : {}),\n ...(createdBy ? { createdBy } : {}),\n });\n\n // Two tags pair as the two roots, under a declared alias, under one role and\n // name, or as the one replacement its author recorded (see `match.ts`). The tag is said first: the\n // role, the attributes and the box that follow are what the new element is,\n // not edits to the old one.\n if (retagged) {\n deltas.push({ ...base('tag-changed'), from: before.tag, to: after.tag });\n }\n\n if (before.role !== after.role) {\n deltas.push({ ...base('role-changed'), from: before.role, to: after.role });\n }\n if (before.name !== after.name) {\n deltas.push({ ...base('name-changed'), from: before.name, to: after.name });\n }\n if (before.description !== after.description) {\n deltas.push({\n ...base('description-changed'),\n from: before.description,\n to: after.description,\n });\n }\n if (before.text !== after.text) {\n deltas.push({ ...base('text-changed'), from: before.text, to: after.text });\n }\n\n for (const property of unionKeys(before.state, after.state)) {\n const from = before.state?.[property];\n const to = after.state?.[property];\n if (from !== to) {\n deltas.push({ ...base('state-changed'), property, from: str(from), to: str(to) });\n }\n }\n\n for (const property of unionKeys(before.attributes, after.attributes)) {\n const from = before.attributes[property];\n const to = after.attributes[property];\n if (from !== to) {\n deltas.push({ ...base('attribute-changed'), property, from, to });\n }\n }\n\n let reflowCause: Delta | null = null;\n\n for (const property of unionKeys(before.style, after.style)) {\n const from = before.style[property];\n const to = after.style[property];\n if (from === to) continue;\n\n // A token whose own value moved makes this delta collateral rather than a\n // root. Recorded here so attribution does not have to re-derive it.\n const token = changedTokenFor(before, after, property);\n const impact = impactOf(property);\n\n const delta: Delta = {\n ...base('style-changed'),\n property,\n from,\n to,\n impact,\n ...(token ? { token } : {}),\n };\n\n deltas.push(delta);\n if (impact === 'layout' && reflowCause === null) reflowCause = delta;\n }\n\n // Which token a property resolves *through* is part of the render hash, so a\n // change in it moves the hash — but under a profile that cannot resolve custom\n // properties both resolved values are the same empty string and the loop above\n // sees nothing. The result was a non-identical diff carrying zero deltas and\n // zero roots: a correct verdict with an empty docket, which tells a reviewer\n // that something changed and then refuses to say what. Found by scoring\n // `prop-size/button` under `jsdom`, which only became scorable with ADR-0008.\n //\n // Skipped when the resolved value moved too: the `style-changed` delta above\n // already names that property, and reporting both splits one cause in two.\n for (const property of unionKeys(before.styleTokens, after.styleTokens)) {\n const from = before.styleTokens?.[property];\n const to = after.styleTokens?.[property];\n if (from === to) continue;\n if (before.style[property] !== after.style[property]) continue;\n\n deltas.push({ ...base('token-changed'), property, from, to });\n }\n\n if (hasLayout && !sameRect(before, after)) {\n // A rect that moved because this node's own padding changed is the same\n // finding observed twice. Folding it under its cause — and inheriting that\n // cause's token — keeps one edit as one docket entry instead of splitting it\n // into a `token` root and an unrelated-looking `geometry` one.\n //\n // A rect that moved with *no* layout-impact change here is different and\n // stays independent: something upstream reflowed and pushed this node, which\n // is exactly the propagation worth surfacing.\n deltas.push({\n ...base('rect-changed'),\n ...(before.rect ? { rectFrom: before.rect } : {}),\n ...(after.rect ? { rectTo: after.rect } : {}),\n ...(reflowCause\n ? {\n derivedFrom: `${reflowCause.path}:${reflowCause.property ?? ''}`,\n ...(reflowCause.token ? { token: reflowCause.token } : {}),\n }\n : {}),\n });\n }\n}\n\n/**\n * The token that explains *this property's* change, if one does.\n *\n * Two conditions, and both are load-bearing. The property must actually resolve\n * through the token — a node whose `color` comes from a token and whose padding\n * comes from a literal is collateral of a token edit only in its colour. And the\n * token's own value must have moved: a node that merely mentions\n * `--color-primary` while that token held is not collateral of anything, its own\n * rule changed, and it is a root.\n *\n * Dropping either check splits one docket entry into several. Ignoring the\n * property meant a rule that overrode a token-driven value got attributed to the\n * token it had just stopped using.\n */\nfunction changedTokenFor(\n before: SemanticNode,\n after: SemanticNode,\n property: string,\n): string | undefined {\n const token = before.styleTokens?.[property];\n\n // The *same* token must drive the property on both sides. A property that\n // stopped resolving through a token did so because some rule started winning\n // instead — that rule is the root, and blaming the abandoned token would name\n // the thing that did not change.\n // Properties whose initial value is `currentColor` follow `color` without ever\n // naming it: no rule declares `outline-color`, so it resolves through no token,\n // yet a token-driven `color` change moves it. Under a profile with computed\n // style that produced a second root per Button — \"the accent token moved\" and\n // \"Button changed\" — for one edit. Recognised by the value matching `color` on\n // both sides, so a node that genuinely declares its own outline colour is\n // unaffected.\n if (token === undefined && FOLLOWS_CURRENT_COLOR.has(property)) {\n const from = before.style[property];\n const to = after.style[property];\n if (from !== undefined && to !== undefined && from === before.style['color'] && to === after.style['color']) {\n return changedTokenFor(before, after, 'color');\n }\n return undefined;\n }\n\n if (token === undefined || after.styleTokens?.[property] !== token) return undefined;\n\n // Undefined on one side counts as a change: a theme override introducing a\n // token that previously had no value is exactly the case the token band exists\n // to collapse into one root.\n if (before.tokens?.[token] !== after.tokens?.[token]) return token;\n\n // The named token held — and may never have had a value at all.\n // `var(--ks-card-radius, var(--va-radius-md))` is the ordinary shape of a\n // component token with a system fallback: `styleTokens` names the outer one,\n // because that is what the author wrote, and when it is undefined the value\n // came from the fallback. Comparing the outer name's value then compares\n // `undefined` with `undefined`, concludes the token held, and reports an edit\n // to the radius *scale* as `component:Card` — \"Card changed internally\", once\n // per consuming component — instead of one token root with counted collateral.\n // That is the product's headline claim failing on the most common\n // design-system shape there is, and it survived because the corpus asserted\n // the root *count*, which is one either way.\n return soleMovedToken(before, after);\n}\n\n/**\n * The one token this node resolved through whose value moved, if there is\n * exactly one.\n *\n * The fallback for a chain whose outer name carries no value. Deliberately\n * refuses to answer when two moved: \"the named token held and several others\n * changed\" is genuinely ambiguous, and picking one would put a confident wrong\n * name in front of a reviewer. Ambiguity falls through to the component root,\n * which is coarser and true.\n */\nfunction soleMovedToken(before: SemanticNode, after: SemanticNode): string | undefined {\n const moved = unionKeys(before.tokens, after.tokens).filter(\n (name) => before.tokens?.[name] !== after.tokens?.[name],\n );\n\n return moved.length === 1 ? moved[0] : undefined;\n}\n\n/**\n * Properties whose initial value is `currentColor`.\n *\n * `color` itself is excluded, or the lookup would recurse.\n */\nconst FOLLOWS_CURRENT_COLOR: ReadonlySet<string> = new Set([\n 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',\n 'outline-color', 'text-decoration-color', 'text-emphasis-color', 'column-rule-color',\n 'caret-color',\n]);\n\nfunction sameRect(before: SemanticNode, after: SemanticNode): boolean {\n const a = before.rect;\n const b = after.rect;\n if (a === undefined || b === undefined) return a === b;\n return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;\n}\n\nfunction unionKeys(\n a: Readonly<Record<string, unknown>> | undefined,\n b: Readonly<Record<string, unknown>> | undefined,\n): readonly string[] {\n return [...new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})])].sort();\n}\n\nfunction str(value: unknown): string | undefined {\n return value === undefined ? undefined : String(value);\n}\n"]}
@@ -6,7 +6,8 @@ import type { SemanticNode } from '../../format/snapshot.js';
6
6
  * Diffing by path alone would report a single insertion as "every following node
7
7
  * changed" — the classic snapshot-test failure that trains people to stop
8
8
  * reading diffs. Matching therefore happens by identity first, content second,
9
- * and position last.
9
+ * and position last; a node no key relates is paired only by the author that
10
+ * recorded writing it.
10
11
  *
11
12
  * The middle pass is the one that took three attempts to find. Identity keys
12
13
  * collapse whenever a node has no alias, no accessible name and no provenance —
@@ -82,6 +82,25 @@ export function matchTrees(baseline, candidate) {
82
82
  if (claimed !== undefined)
83
83
  partner[afterIndex] = claimed;
84
84
  });
85
+ // Pass 3 — a replacement its author recorded. A component that swaps its
86
+ // own output, `Toggle` rendering a `<div>` where it rendered a checkbox,
87
+ // leaves two nodes no key relates: they share no content, and one is a
88
+ // named role where the other is a bare tag. What relates them is who wrote
89
+ // them. Under one matched parent, one node left and one arrived, and the
90
+ // same component created both, so the second is read as what that
91
+ // component rendered instead of the first. The record cannot tell that from
92
+ // an element dropped and an unrelated one drawn elsewhere in the row, and
93
+ // the pair's position is not asked: the deltas then say what the slot
94
+ // became and that it moved.
95
+ //
96
+ // Exactly one and one, or nothing. Two that left and two that arrived from
97
+ // one author could pair either way, and choosing by position is the guess
98
+ // pass 2 already declines across keys. And never two nodes that each
99
+ // declare who they are: `createdBy` names a component, and every `<Button>`
100
+ // is created by `Button`, so a Save button and an Export link under one
101
+ // toolbar share an author while their names, which pass 2 has read, say
102
+ // they are two elements.
103
+ replacements(before.children, after.children, taken, partner);
85
104
  const sequence = [];
86
105
  after.children.forEach((child, afterIndex) => {
87
106
  const beforeIndex = partner[afterIndex];
@@ -105,6 +124,39 @@ export function matchTrees(baseline, candidate) {
105
124
  });
106
125
  }
107
126
  }
127
+ /** Pairs, in place, the one unclaimed node on each side that one author created. */
128
+ function replacements(before, after, taken, partner) {
129
+ const left = byAuthor(before, (position) => !taken.has(position));
130
+ const arrived = byAuthor(after, (position) => partner[position] === undefined);
131
+ for (const [author, [beforeIndex, ...more]] of left) {
132
+ const counterpart = arrived.get(author);
133
+ if (more.length > 0 || counterpart?.length !== 1)
134
+ continue;
135
+ if (declared(before[beforeIndex]) && declared(after[counterpart[0]]))
136
+ continue;
137
+ taken.add(beforeIndex);
138
+ partner[counterpart[0]] = beforeIndex;
139
+ }
140
+ }
141
+ /** Whether a node says who it is, by the first two of `matchKey`'s keys. */
142
+ function declared(node) {
143
+ return node.alias !== undefined || (node.role !== undefined && node.name !== undefined);
144
+ }
145
+ /** Positions of the nodes `open` admits, by the component that created each. */
146
+ function byAuthor(children, open) {
147
+ const authors = new Map();
148
+ children.forEach((child, position) => {
149
+ const author = child.provenance?.createdBy;
150
+ if (author === undefined || !open(position))
151
+ return;
152
+ const positions = authors.get(author);
153
+ if (positions)
154
+ positions.push(position);
155
+ else
156
+ authors.set(author, [position]);
157
+ });
158
+ return authors;
159
+ }
108
160
  function index(children, key) {
109
161
  const buckets = new Map();
110
162
  children.forEach((child, position) => {