@variance-authority/core 0.14.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +878 -0
  2. package/README.md +38 -17
  3. package/dist/attribute/component-hash.d.ts +14 -5
  4. package/dist/attribute/component-hash.js +14 -5
  5. package/dist/attribute/component-hash.js.map +1 -1
  6. package/dist/attribute/composition.d.ts +16 -0
  7. package/dist/attribute/composition.js +4 -6
  8. package/dist/attribute/composition.js.map +1 -1
  9. package/dist/attribute/index.d.ts +3 -1
  10. package/dist/attribute/index.js +2 -1
  11. package/dist/attribute/index.js.map +1 -1
  12. package/dist/attribute/movement.d.ts +1 -1
  13. package/dist/attribute/movement.js.map +1 -1
  14. package/dist/attribute/region.d.ts +47 -4
  15. package/dist/attribute/region.js +91 -22
  16. package/dist/attribute/region.js.map +1 -1
  17. package/dist/attribute/source.d.ts +10 -24
  18. package/dist/attribute/source.js +4 -38
  19. package/dist/attribute/source.js.map +1 -1
  20. package/dist/attribute/subject-pieces.d.ts +37 -0
  21. package/dist/attribute/subject-pieces.js +122 -0
  22. package/dist/attribute/subject-pieces.js.map +1 -0
  23. package/dist/compare/band.d.ts +8 -2
  24. package/dist/compare/band.js +7 -2
  25. package/dist/compare/band.js.map +1 -1
  26. package/dist/compare/diff/attribution.js +6 -4
  27. package/dist/compare/diff/attribution.js.map +1 -1
  28. package/dist/compare/diff/compare-nodes.js +9 -1
  29. package/dist/compare/diff/compare-nodes.js.map +1 -1
  30. package/dist/compare/diff/match.d.ts +2 -1
  31. package/dist/compare/diff/match.js +52 -0
  32. package/dist/compare/diff/match.js.map +1 -1
  33. package/dist/compare/parting.js +6 -4
  34. package/dist/compare/parting.js.map +1 -1
  35. package/dist/format/capture.d.ts +25 -0
  36. package/dist/format/capture.js +6 -0
  37. package/dist/format/capture.js.map +1 -1
  38. package/dist/format/document.d.ts +28 -5
  39. package/dist/format/document.js +8 -2
  40. package/dist/format/document.js.map +1 -1
  41. package/dist/format/drive.d.ts +191 -0
  42. package/dist/format/drive.js +343 -0
  43. package/dist/format/drive.js.map +1 -0
  44. package/dist/format/environment.d.ts +20 -2
  45. package/dist/format/environment.js +7 -0
  46. package/dist/format/environment.js.map +1 -1
  47. package/dist/format/index.d.ts +13 -3
  48. package/dist/format/index.js +8 -2
  49. package/dist/format/index.js.map +1 -1
  50. package/dist/format/intervention.d.ts +79 -0
  51. package/dist/format/intervention.js.map +1 -1
  52. package/dist/format/origin.d.ts +32 -0
  53. package/dist/format/origin.js +54 -0
  54. package/dist/format/origin.js.map +1 -0
  55. package/dist/format/packed.d.ts +32 -0
  56. package/dist/format/packed.js +23 -0
  57. package/dist/format/packed.js.map +1 -0
  58. package/dist/format/shape.d.ts +31 -0
  59. package/dist/format/shape.js +269 -0
  60. package/dist/format/shape.js.map +1 -0
  61. package/dist/format/snapshot.d.ts +2 -0
  62. package/dist/format/snapshot.js.map +1 -1
  63. package/dist/format/stabilize.d.ts +35 -6
  64. package/dist/format/stabilize.js +58 -103
  65. package/dist/format/stabilize.js.map +1 -1
  66. package/dist/format/steps.d.ts +44 -0
  67. package/dist/format/steps.js +67 -0
  68. package/dist/format/steps.js.map +1 -0
  69. package/dist/format/waits.d.ts +12 -0
  70. package/dist/format/waits.js +156 -0
  71. package/dist/format/waits.js.map +1 -0
  72. package/dist/format/width.d.ts +35 -0
  73. package/dist/format/width.js +38 -0
  74. package/dist/format/width.js.map +1 -0
  75. package/dist/index.d.ts +2 -1
  76. package/dist/index.js +2 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/judge/docket.js +1 -0
  79. package/dist/judge/docket.js.map +1 -1
  80. package/dist/judge/index.d.ts +1 -0
  81. package/dist/judge/index.js +1 -0
  82. package/dist/judge/index.js.map +1 -1
  83. package/dist/judge/restamp.d.ts +20 -0
  84. package/dist/judge/restamp.js +20 -0
  85. package/dist/judge/restamp.js.map +1 -0
  86. package/dist/nearest.d.ts +46 -0
  87. package/dist/nearest.js +102 -0
  88. package/dist/nearest.js.map +1 -0
  89. package/dist/relate/before.d.ts +19 -24
  90. package/dist/relate/before.js +37 -39
  91. package/dist/relate/before.js.map +1 -1
  92. package/dist/relate/beyond.d.ts +59 -0
  93. package/dist/relate/beyond.js +98 -0
  94. package/dist/relate/beyond.js.map +1 -0
  95. package/dist/relate/carried.d.ts +122 -0
  96. package/dist/relate/carried.js +279 -0
  97. package/dist/relate/carried.js.map +1 -0
  98. package/dist/relate/graph.d.ts +14 -12
  99. package/dist/relate/graph.js +12 -11
  100. package/dist/relate/graph.js.map +1 -1
  101. package/dist/relate/index.d.ts +3 -1
  102. package/dist/relate/index.js +3 -1
  103. package/dist/relate/index.js.map +1 -1
  104. package/dist/relate/merkle.js +2 -11
  105. package/dist/relate/merkle.js.map +1 -1
  106. package/dist/relate/narrow.d.ts +2 -2
  107. package/dist/relate/narrow.js +26 -73
  108. package/dist/relate/narrow.js.map +1 -1
  109. package/dist/relate/reach.d.ts +6 -6
  110. package/dist/relate/reach.js +10 -53
  111. package/dist/relate/reach.js.map +1 -1
  112. package/dist/relate/records.d.ts +6 -5
  113. package/dist/relate/records.js +3 -2
  114. package/dist/relate/records.js.map +1 -1
  115. package/dist/rules/index.d.ts +3 -3
  116. package/dist/rules/index.js +3 -3
  117. package/dist/rules/index.js.map +1 -1
  118. package/dist/rules/normalize/cascade.d.ts +12 -0
  119. package/dist/rules/normalize/cascade.js +34 -16
  120. package/dist/rules/normalize/cascade.js.map +1 -1
  121. package/dist/rules/normalize/fixture.d.ts +3 -0
  122. package/dist/rules/normalize/fixture.js +3 -1
  123. package/dist/rules/normalize/fixture.js.map +1 -1
  124. package/dist/rules/normalize/index.d.ts +2 -2
  125. package/dist/rules/normalize/index.js +9 -2
  126. package/dist/rules/normalize/index.js.map +1 -1
  127. package/dist/rules/normalize/value.d.ts +13 -2
  128. package/dist/rules/normalize/value.js +42 -8
  129. package/dist/rules/normalize/value.js.map +1 -1
  130. package/dist/rules/normalize/wrapper.js +5 -0
  131. package/dist/rules/normalize/wrapper.js.map +1 -1
  132. package/dist/rules/ruleset.d.ts +24 -4
  133. package/dist/rules/ruleset.js +24 -4
  134. package/dist/rules/ruleset.js.map +1 -1
  135. package/dist/segment/index.d.ts +20 -4
  136. package/dist/segment/index.js +28 -7
  137. package/dist/segment/index.js.map +1 -1
  138. package/dist/shard/index.d.ts +11 -0
  139. package/dist/shard/index.js +11 -0
  140. package/dist/shard/index.js.map +1 -0
  141. package/dist/shard/placement.d.ts +119 -0
  142. package/dist/shard/placement.js +156 -0
  143. package/dist/shard/placement.js.map +1 -0
  144. package/dist/share/index.d.ts +1 -1
  145. package/dist/share/index.js +1 -1
  146. package/dist/share/index.js.map +1 -1
  147. package/package.json +8 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/attribute/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAU3E,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,EACL,aAAa,EACb,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9E,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGvF,OAAO,EACL,cAAc,EACd,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,GACb,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGhE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG5D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAanD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,eAAe,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACnH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAI7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAG5E,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * `@variance-authority/core/attribute` — from a position to a name to a file.\n *\n * The group that makes a difference actionable. Pixels cluster into regions,\n * regions join the box tree, the tree knows which component produced each node,\n * and the component resolves to a source location. The same machinery answers\n * \"which component is not holding still\" and \"which subjects cover this\n * component\".\n *\n * This is the answer to *\"a pixel differ reports 1530 pixels changed\"* — and the\n * reason it is a group rather than a function is that every step of the journey\n * is separately useful, and separately capable of refusing. `locateInstability`\n * will not name a component for a difference that is beneath the semantic tier;\n * `coverage` will not recommend deleting anything.\n */\n\nexport { locate } from './locate.js';\nexport type { Location, LocationStep } from './locate.js';\n\nexport { isolateRegions, subtractRegions, excludedBoxes } from './mask.js';\nexport type {\n ChangeMask,\n DiffRegion,\n ExcludedBox,\n Isolation,\n IsolationOptions,\n Subtraction,\n} from './mask.js';\n\nexport { attributeRegions, rankRegions } from './region.js';\nexport type { AttributedRegion, AttributionOptions, RankedRegion } from './region.js';\n\nexport {\n resolveSource,\n formatSource,\n indexSource,\n mergeSourceIndexes,\n overlaySourceIndex,\n} from './source.js';\nexport type { SourceIndex, SourceRef, Resolution } from './source.js';\n\nexport { parseStackFrames, isVendorPath, writerLocationOf } from './stack.js';\nexport type { StackFrame } from './stack.js';\n\nexport {\n parseSourceMap,\n originalPositionFor,\n sourceMappingUrlOf,\n inlineSourceMapOf,\n} from './source-map.js';\nexport type { SourceMap, OriginalPosition } from './source-map.js';\n\nexport { createCallSiteResolver, locateProvenance, locateSites } from './call-site.js';\nexport type { CallSiteResolver, CallSiteStats, FetchModule, LocatableSite } from './call-site.js';\n\nexport {\n hashComponents,\n causesBetween,\n movedBands,\n movedBandsBetween,\n bandsBetween,\n UNATTRIBUTED,\n} from './component-hash.js';\nexport type { BandDigests, ComponentBands, ComponentHash, ValueChange } from './component-hash.js';\n\nexport { componentInstances, attributed } from './instances.js';\nexport type { ComponentInstance } from './instances.js';\n\nexport { nodeClosures, sharedClosures } from './closure.js';\nexport type { ClosureSite, NodeClosure, SharedClosure, SharedClosureOptions, SubjectTree } from './closure.js';\n\nexport { composeSubjects } from './composition.js';\nexport type {\n Composition,\n ComponentEntry,\n Echo,\n PropsClass,\n Rendering,\n Site,\n SubjectComposition,\n} from './composition.js';\n\nexport type { Divergence, DivergenceParting } from './divergence.js';\n\nexport { lexiconOf, lexiconOfValues, lexiconValuesOf, structureOf, withExamples, LEXICON_CAP } from './lexicon.js';\nexport { LANDMARK_CAP } from './landmark.js';\nexport type { Landmark } from './landmark.js';\nexport type { BoundaryRow, LexiconField, LexiconOptions, LexiconValues, SubjectLexicon } from './lexicon.js';\n\nexport { attributeMovement } from './movement.js';\nexport type { Attribution, Cause, Evidence, Moved, Movement } from './movement.js';\n\nexport { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';\nexport type { Coverage, ComponentCoverage, SubjectCoverage, SubjectValue } from './coverage.js';\n\nexport { locateInstability, summarizeInstability } from './instability.js';\nexport type {\n Instability,\n InstabilityBand,\n UnstableLocation,\n UnstableProperty,\n} from './instability.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/attribute/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAU3E,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,EACL,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9E,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGvF,OAAO,EACL,cAAc,EACd,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,GACb,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGhE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG5D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAanD,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG/C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,eAAe,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACnH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAI7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAG5E,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * `@variance-authority/core/attribute` — from a position to a name to a file.\n *\n * The group that makes a difference actionable. Pixels cluster into regions,\n * regions join the box tree, the tree knows which component produced each node,\n * and the component resolves to a source location. The same machinery answers\n * \"which component is not holding still\" and \"which subjects cover this\n * component\".\n *\n * This is the answer to *\"a pixel differ reports 1530 pixels changed\"* — and the\n * reason it is a group rather than a function is that every step of the journey\n * is separately useful, and separately capable of refusing. `locateInstability`\n * will not name a component for a difference that is beneath the semantic tier;\n * `coverage` will not recommend deleting anything.\n */\n\nexport { locate } from './locate.js';\nexport type { Location, LocationStep } from './locate.js';\n\nexport { isolateRegions, subtractRegions, excludedBoxes } from './mask.js';\nexport type {\n ChangeMask,\n DiffRegion,\n ExcludedBox,\n Isolation,\n IsolationOptions,\n Subtraction,\n} from './mask.js';\n\nexport { attributeRegions, rankRegions } from './region.js';\nexport type { AttributedRegion, AttributionOptions, RankedRegion } from './region.js';\n\nexport {\n resolveSource,\n formatSource,\n mergeSourceIndexes,\n overlaySourceIndex,\n} from './source.js';\nexport type { SourceIndex, SourceRef, Resolution } from './source.js';\n\nexport { parseStackFrames, isVendorPath, writerLocationOf } from './stack.js';\nexport type { StackFrame } from './stack.js';\n\nexport {\n parseSourceMap,\n originalPositionFor,\n sourceMappingUrlOf,\n inlineSourceMapOf,\n} from './source-map.js';\nexport type { SourceMap, OriginalPosition } from './source-map.js';\n\nexport { createCallSiteResolver, locateProvenance, locateSites } from './call-site.js';\nexport type { CallSiteResolver, CallSiteStats, FetchModule, LocatableSite } from './call-site.js';\n\nexport {\n hashComponents,\n causesBetween,\n movedBands,\n movedBandsBetween,\n bandsBetween,\n UNATTRIBUTED,\n} from './component-hash.js';\nexport type { BandDigests, ComponentBands, ComponentHash, ValueChange } from './component-hash.js';\n\nexport { componentInstances, attributed } from './instances.js';\nexport type { ComponentInstance } from './instances.js';\n\nexport { nodeClosures, sharedClosures } from './closure.js';\nexport type { ClosureSite, NodeClosure, SharedClosure, SharedClosureOptions, SubjectTree } from './closure.js';\n\nexport { composeSubjects } from './composition.js';\nexport type {\n Composition,\n ComponentEntry,\n Echo,\n PropsClass,\n Rendering,\n Site,\n SubjectComposition,\n} from './composition.js';\n\nexport type { Divergence, DivergenceParting } from './divergence.js';\n\nexport { piecesOf } from './subject-pieces.js';\nexport type { Residue, SubjectPieces, SubjectShare } from './subject-pieces.js';\n\nexport { lexiconOf, lexiconOfValues, lexiconValuesOf, structureOf, withExamples, LEXICON_CAP } from './lexicon.js';\nexport { LANDMARK_CAP } from './landmark.js';\nexport type { Landmark } from './landmark.js';\nexport type { BoundaryRow, LexiconField, LexiconOptions, LexiconValues, SubjectLexicon } from './lexicon.js';\n\nexport { attributeMovement } from './movement.js';\nexport type { Attribution, Cause, Evidence, Moved, Movement } from './movement.js';\n\nexport { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';\nexport type { Coverage, ComponentCoverage, SubjectCoverage, SubjectValue } from './coverage.js';\n\nexport { locateInstability, summarizeInstability } from './instability.js';\nexport type {\n Instability,\n InstabilityBand,\n UnstableLocation,\n UnstableProperty,\n} from './instability.js';\n"]}
@@ -42,7 +42,7 @@ import type { Composition, Site } from './composition.js';
42
42
  * Unexplained is not a flake. This system's position on what establishes a flake
43
43
  * has not moved: a subject is unstable when it fails to read the same way twice,
44
44
  * and one reading can never prove instability
45
- * ([`flakiness.md`](../../../../docs/flakiness.md)). What an unexplained movement
45
+ * ([`flakiness.md`](https://variance-authority.dev/docs/flakiness)). What an unexplained movement
46
46
  * is, exactly, is **the shortlist of subjects worth reading twice** — which is
47
47
  * the scarce resource a sweep spends, and until now it was spent on `changed`
48
48
  * subjects in whatever order the plan produced them.
@@ -1 +1 @@
1
- {"version":3,"file":"movement.js","sourceRoot":"","sources":["../../src/attribute/movement.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Band } from '../compare/band.js';\nimport type { Composition, ComponentEntry, Site } from './composition.js';\nimport { becauseUpstream, unexplainedBecause } from './because.js';\nimport { heldSites } from './control.js';\n\n/**\n * Why a component moved — and what it means when nothing here can say.\n *\n * A run already knows *that* a component's own content differs from its\n * baseline: that is `causesBetween`, and it is where every report starts. What it\n * has never known is whether anybody edited the thing. Those are different\n * questions, and the gap between them is where flakes live:\n *\n * > Detect a pixel change. Find the HTML area behind it. Find no related change.\n * > That is a flake — or the beginning of one.\n *\n * The last step is the one that needs evidence, and the evidence has to be\n * assembled rather than assumed. Four kinds are available to a run that has\n * already done its work, in descending order of how much they explain:\n *\n * 1. **An edit.** A file declaring this component is in the change set. Nothing\n * further is interesting; somebody changed it and the run noticed.\n * 2. **A token.** A custom property this component's own nodes resolve through\n * took a new value in this run. The component's code is untouched and its\n * output legitimately moved.\n * 3. **An ancestor.** A component that reaches it was edited, and it is reached\n * by climbing — the caller that wrote the element, or, on a build where React\n * kept no owner, the enclosure graph of the subject it moved in until an\n * edited name appears. Its props are not recoverable in detail, but *this\n * moved on what it was given* is the sentence, and it names a real file.\n * 4. **A contradiction.** The same component, with the same props, rendered\n * somewhere else in this same run and rendered *differently*. That is not an\n * explanation — it is proof the explanation is not in the component's own\n * code, because one commit produced two answers from one input.\n *\n * And then **unexplained**, which is the finding. A component whose content\n * moved, whose file nobody touched, whose tokens held, whose ancestors held, and\n * which rendered identically everywhere else in the suite at this commit. That\n * last clause is the part no previous version of this could say, and it is what\n * the cross-subject join bought: the *stable states to refer to* are the other\n * sites of the same rendering, and the run already has them.\n *\n * ## What this refuses to conclude\n *\n * Unexplained is not a flake. This system's position on what establishes a flake\n * has not moved: a subject is unstable when it fails to read the same way twice,\n * and one reading can never prove instability\n * ([`flakiness.md`](../../../../docs/flakiness.md)). What an unexplained movement\n * is, exactly, is **the shortlist of subjects worth reading twice** — which is\n * the scarce resource a sweep spends, and until now it was spent on `changed`\n * subjects in whatever order the plan produced them.\n */\n\nexport type Cause = 'edited' | 'token' | 'upstream' | 'contradicted' | 'unexplained';\n\n/** One component the run found to have moved in one subject. */\nexport interface Moved {\n readonly subject: string;\n readonly component: string;\n /**\n * Which bands moved, when the run could tell.\n *\n * Empty when the comparison produced names only — a baseline written before\n * the bands were split carries no per-band digests, and an empty list here\n * means *not known*, never *no band*.\n */\n readonly bands: readonly Band[];\n}\n\n/** What a run can put beside a movement to explain it. */\nexport interface Evidence {\n /**\n * Files in the change set, from whichever flag read a diff. Absent when none did.\n *\n * Absent and empty are different answers and are kept different: a run with no\n * `--since` has not established that nobody edited anything, and reading its\n * silence as \"no edits\" would attribute every movement in the suite to nothing\n * and call the result a flake list.\n */\n readonly changed?: readonly string[];\n\n /** Component name → the files that declare it, from the source index. */\n readonly declaredIn?: ReadonlyMap<string, readonly string[]>;\n\n /** Tokens whose resolved value moved in this run. Absent when nothing asked. */\n readonly tokens?: readonly string[];\n\n /** Subjects that failed to read the same way twice in this run. */\n readonly unstable?: ReadonlySet<string>;\n\n /**\n * Every component whose hashes moved, by subject — the whole reading, not the\n * causes.\n *\n * The control group is read from this and not from the movements, because the\n * movements are a *filtered* set: they come from regions the pixels named as\n * causes, so a component whose hashes moved and whose box no region named is\n * absent from them. Filtering the control group by the movements alone hands\n * back exactly those renders as *held still*, which is the one claim that\n * makes a control group worse than no control group.\n *\n * **Absent is not empty**, twice over. An absent map is a run that never\n * compared hashes, and the control group falls back to the movements with a\n * weaker guarantee. A subject *missing from* a present map had no hashes read\n * there — nobody measured it, so it cannot be a control for anything.\n */\n readonly hashesMoved?: ReadonlyMap<string, ReadonlySet<string>>;\n}\n\nexport interface Movement {\n readonly subject: string;\n readonly component: string;\n readonly bands: readonly Band[];\n readonly cause: Cause;\n /** One sentence, naming the evidence rather than the category. */\n readonly because: string;\n\n readonly file?: string;\n readonly tokens?: readonly string[];\n readonly upstream?: string;\n\n /**\n * Components between `upstream` and this one, outermost first.\n *\n * Empty when the edited component draws it directly, and present only on the\n * `upstream` rung. A reviewer sent to `ProductCard` for a `CardFooter` that\n * moved has one question — *how does that reach this* — and the answer is two\n * names the run already holds.\n */\n readonly through?: readonly string[];\n\n /**\n * Other subjects in which this same component moved in this run.\n *\n * The \"one cause, N subjects\" fold. A reviewer reading eleven changed subjects\n * is often reading one edit, and the count is the difference between a\n * frightening report and an accurate one.\n */\n readonly alsoIn: readonly string[];\n\n /**\n * Sites of this component, with the same props, whose hashes this run read and\n * found unmoved.\n *\n * The control group, and the reason an unexplained movement is worth\n * reporting rather than shrugging at. Empty means there was no control — the\n * component appears nowhere else with these inputs, or it moved in every one\n * of them — which weakens the finding and is why it is a list rather than a\n * flag.\n */\n readonly held: readonly Site[];\n\n /**\n * Sites of this component with the same props, other than this one, that the\n * run read — the pool `held` was drawn from.\n *\n * The denominator, and without it `held: []` is two opposite findings under one\n * shape. Zero is *nothing to compare against*; four with an empty `held` is a\n * comparison that was made, in four renders, and came back the same way in all\n * of them — the statement that nothing rendered under those props escaped\n * whatever moved here.\n */\n readonly compared: number;\n}\n\nexport interface Attribution {\n /** Every movement supplied, in the order supplied, each with a cause. */\n readonly movements: readonly Movement[];\n\n /**\n * Unexplained movements in subjects that also failed to read the same way\n * twice. A movement nothing explains, in a subject already proven unstable.\n */\n readonly flakes: readonly Movement[];\n\n /**\n * Unexplained movements in subjects nothing has read twice.\n *\n * Not a verdict. The shortlist a sweep should spend its second readings on,\n * ordered by how much control the suite has over each — a component that\n * rendered identically in six other places and moved here is a better use of a\n * second reading than one that appears nowhere else.\n */\n readonly suspects: readonly Movement[];\n}\n\n/**\n * Attribute every movement, given whatever evidence the run assembled.\n *\n * Degrades honestly. With no change set the `edited` rung is unreachable and\n * every movement falls to a lower one, so a run that did not ask cannot produce\n * a confident `unexplained` — which is checked here rather than trusted to the\n * caller, because the shortlist this feeds is exactly the thing somebody will\n * act on.\n */\nexport function attributeMovement(\n moved: readonly Moved[],\n composition: Composition,\n evidence: Evidence = {},\n): Attribution {\n const byComponent = new Map(composition.components.map((entry) => [entry.component, entry]));\n const movedIn = new Map<string, string[]>();\n for (const each of moved) {\n const subjects = movedIn.get(each.component) ?? [];\n subjects.push(each.subject);\n movedIn.set(each.component, subjects);\n }\n\n const edited = new Set(\n [...(evidence.declaredIn ?? new Map<string, readonly string[]>())]\n .filter(([, files]) => files.some((file) => (evidence.changed ?? []).includes(file)))\n .map(([component]) => component),\n );\n\n const graph = graphOf(composition);\n\n const movements = moved.map((each) =>\n attributeOne(each, byComponent.get(each.component), {\n movedIn,\n edited,\n evidence,\n composition,\n graph,\n }),\n );\n\n const unexplained = movements.filter((movement) => movement.cause === 'unexplained');\n\n return {\n movements,\n flakes: unexplained.filter((movement) => evidence.unstable?.has(movement.subject) === true),\n suspects: unexplained\n .filter((movement) => evidence.unstable?.has(movement.subject) !== true)\n .sort((a, b) => b.held.length - a.held.length),\n };\n}\n\n/** Everything the run assembled, gathered so one movement can be asked about. */\nexport interface Bench {\n readonly movedIn: ReadonlyMap<string, readonly string[]>;\n readonly edited: ReadonlySet<string>;\n readonly evidence: Evidence;\n readonly composition: Composition;\n readonly graph: Graph;\n}\n\nfunction attributeOne(moved: Moved, entry: ComponentEntry | undefined, bench: Bench): Movement {\n const { movedIn, edited, evidence, composition, graph } = bench;\n const alsoIn = (movedIn.get(moved.component) ?? []).filter(\n (subject) => subject !== moved.subject,\n );\n const { held, considered } = heldSites(moved, entry, bench);\n const base = { ...moved, alsoIn, held, compared: considered };\n\n const file = (evidence.declaredIn?.get(moved.component) ?? []).find((each) =>\n (evidence.changed ?? []).includes(each),\n );\n if (file !== undefined) {\n return { ...base, cause: 'edited', file, because: `\\`${file}\\` is in the change set` };\n }\n\n const tokens = movedTokens(moved.component, composition, evidence);\n if (tokens.length > 0) {\n return {\n ...base,\n cause: 'token',\n tokens,\n because: `it resolves through ${tokens.map((token) => `\\`${token}\\``).join(', ')}, which moved in this run`,\n };\n }\n\n const ancestor = editedAncestor(moved, edited, graph);\n if (ancestor !== undefined) {\n return {\n ...base,\n cause: 'upstream',\n upstream: ancestor.name,\n ...(ancestor.through.length === 0 ? {} : { through: ancestor.through }),\n because: becauseUpstream(ancestor),\n };\n }\n\n const contradiction = composition.divergences.find(\n (divergence) =>\n divergence.component === moved.component &&\n divergence.renderings.some((rendering) =>\n rendering.sites.some((site) => site.subject === moved.subject),\n ),\n );\n if (contradiction !== undefined) {\n return {\n ...base,\n cause: 'contradicted',\n because:\n `at this commit it renders ${contradiction.renderings.length} different ways from the same props ` +\n `(${contradiction.bands.join(', ')}), so the change is not in its own code`,\n };\n }\n\n return { ...base, cause: 'unexplained', because: unexplainedBecause(base, considered, evidence) };\n}\n\n/**\n * Who encloses what and who wrote what, in each subject separately.\n *\n * Keyed on the pair because the constraint is the whole point. `ComponentEntry.within`\n * and `ComponentEntry.createdBy` are both folded over the suite, so `Card` is enclosed by\n * `CartCard` and by `ProductCard` at once, and a walk that reads either hands a\n * reviewer the cart as the reason a product story moved. A site knows which\n * subject it was read in, so the graph can be built the way the question is\n * asked — and it has to be built that way for *both* edges, because the creator\n * is consulted first and an unconstrained answer there is never reached by the\n * constrained walk underneath it.\n */\ntype Enclosures = ReadonlyMap<string, readonly string[]>;\n\ninterface Graph {\n /** Who this component sits inside, per subject. */\n readonly within: Enclosures;\n /** Whose JSX wrote this component's element, per subject. */\n readonly wrote: Enclosures;\n}\n\nconst IN = '\\u0000';\n\nfunction graphOf(composition: Composition): Graph {\n const within = new Map<string, string[]>();\n const wrote = new Map<string, string[]>();\n\n const add = (graph: Map<string, string[]>, key: string, name: string): void => {\n const names = graph.get(key);\n if (names === undefined) graph.set(key, [name]);\n else if (!names.includes(name)) names.push(name);\n };\n\n for (const entry of composition.components) {\n for (const group of entry.classes) {\n for (const rendering of group.renderings) {\n for (const site of rendering.sites) {\n const key = `${site.subject}${IN}${entry.component}`;\n if (site.within !== undefined) add(within, key, site.within);\n if (site.createdBy !== undefined) add(wrote, key, site.createdBy);\n }\n }\n }\n }\n\n for (const names of within.values()) names.sort();\n for (const names of wrote.values()) names.sort();\n return { within, wrote };\n}\n\n/** An edited component above a moved one, and the components in between. */\nexport interface Ancestor {\n readonly name: string;\n /** Outermost first, and empty when the edited component draws it directly. */\n readonly through: readonly string[];\n}\n\n/**\n * The nearest edited component above this one, in the subject it moved in.\n *\n * The creator first, and it is not a tie-break. The component that *wrote the\n * element* is the one whose edit changed this component's inputs; the one it\n * happens to sit inside may be a presentational wrapper that knows nothing about\n * it. On `examples/todomvc` every `Chip` sits within a `Stack` and is created by\n * `TodoFooter`, so a run consulting only enclosure would fail to connect an edit\n * to `TodoFooter` with the chips it moved — and report five unexplained movements\n * instead of one caller. It is absent on a production build, which is why the\n * enclosure walk is not a fallback but the other half.\n *\n * Read off the per-subject graph and not off the entry. The entry's `createdBy`\n * is the suite's whole set, so an edit to whoever writes this component on one\n * page answers for every page it appears on — the same unconstrained answer the\n * walk below refuses, arriving one line earlier and winning.\n *\n * And that walk climbs rather than looking once. The rung has always said\n * *ancestor* and checked a parent, and a React tree is mostly components that\n * draw one wrapper each: `ProductCard` was edited, `ProductCard` draws `Card`,\n * `Card` draws `CardFooter`, and one look up arrives at `Card`, whose file nobody\n * touched. That is the shape of every unexplained movement this ladder used to\n * produce over an edit it was holding the graph for.\n */\nfunction editedAncestor(\n moved: Moved,\n edited: ReadonlySet<string>,\n graph: Graph,\n): Ancestor | undefined {\n const here = `${moved.subject}${IN}${moved.component}`;\n const wrote = (graph.wrote.get(here) ?? []).find((name) => edited.has(name));\n if (wrote !== undefined) return { name: wrote, through: [] };\n\n // Breadth-first, so the answer is the *nearest* edit and not whichever one the\n // recursion reached first. `seen` is written as a rung is built: an enclosure\n // graph folded over a suite can close a loop, and a component reached twice on\n // one rung would otherwise queue twice.\n const seen = new Set([moved.component]);\n const step = (from: Ancestor | null): Ancestor[] => {\n const child = from === null ? moved.component : from.name;\n const rung: Ancestor[] = [];\n for (const holder of graph.within.get(`${moved.subject}${IN}${child}`) ?? []) {\n if (seen.has(holder)) continue;\n seen.add(holder);\n rung.push({ name: holder, through: from === null ? [] : [from.name, ...from.through] });\n }\n return rung;\n };\n\n let rung = step(null);\n while (rung.length > 0) {\n const arrived = rung.find((each) => edited.has(each.name));\n if (arrived !== undefined) return arrived;\n rung = rung.flatMap(step);\n }\n\n return undefined;\n}\n\n/**\n * Tokens this component resolves through that moved in this run.\n *\n * Read off the instances rather than off the subject, which is the distinction\n * that makes the answer worth anything: every subject on a themed page resolves\n * through every token in the theme, so a subject-level intersection names them\n * all and explains nothing.\n */\nfunction movedTokens(\n component: string,\n composition: Composition,\n evidence: Evidence,\n): readonly string[] {\n if (evidence.tokens === undefined || evidence.tokens.length === 0) return [];\n\n const entry = composition.components.find((each) => each.component === component);\n if (entry === undefined) return [];\n\n const moved = new Set(evidence.tokens);\n return [...entry.tokens].filter((token) => moved.has(token)).sort();\n}\n"]}
1
+ {"version":3,"file":"movement.js","sourceRoot":"","sources":["../../src/attribute/movement.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Band } from '../compare/band.js';\nimport type { Composition, ComponentEntry, Site } from './composition.js';\nimport { becauseUpstream, unexplainedBecause } from './because.js';\nimport { heldSites } from './control.js';\n\n/**\n * Why a component moved — and what it means when nothing here can say.\n *\n * A run already knows *that* a component's own content differs from its\n * baseline: that is `causesBetween`, and it is where every report starts. What it\n * has never known is whether anybody edited the thing. Those are different\n * questions, and the gap between them is where flakes live:\n *\n * > Detect a pixel change. Find the HTML area behind it. Find no related change.\n * > That is a flake — or the beginning of one.\n *\n * The last step is the one that needs evidence, and the evidence has to be\n * assembled rather than assumed. Four kinds are available to a run that has\n * already done its work, in descending order of how much they explain:\n *\n * 1. **An edit.** A file declaring this component is in the change set. Nothing\n * further is interesting; somebody changed it and the run noticed.\n * 2. **A token.** A custom property this component's own nodes resolve through\n * took a new value in this run. The component's code is untouched and its\n * output legitimately moved.\n * 3. **An ancestor.** A component that reaches it was edited, and it is reached\n * by climbing — the caller that wrote the element, or, on a build where React\n * kept no owner, the enclosure graph of the subject it moved in until an\n * edited name appears. Its props are not recoverable in detail, but *this\n * moved on what it was given* is the sentence, and it names a real file.\n * 4. **A contradiction.** The same component, with the same props, rendered\n * somewhere else in this same run and rendered *differently*. That is not an\n * explanation — it is proof the explanation is not in the component's own\n * code, because one commit produced two answers from one input.\n *\n * And then **unexplained**, which is the finding. A component whose content\n * moved, whose file nobody touched, whose tokens held, whose ancestors held, and\n * which rendered identically everywhere else in the suite at this commit. That\n * last clause is the part no previous version of this could say, and it is what\n * the cross-subject join bought: the *stable states to refer to* are the other\n * sites of the same rendering, and the run already has them.\n *\n * ## What this refuses to conclude\n *\n * Unexplained is not a flake. This system's position on what establishes a flake\n * has not moved: a subject is unstable when it fails to read the same way twice,\n * and one reading can never prove instability\n * ([`flakiness.md`](https://variance-authority.dev/docs/flakiness)). What an unexplained movement\n * is, exactly, is **the shortlist of subjects worth reading twice** — which is\n * the scarce resource a sweep spends, and until now it was spent on `changed`\n * subjects in whatever order the plan produced them.\n */\n\nexport type Cause = 'edited' | 'token' | 'upstream' | 'contradicted' | 'unexplained';\n\n/** One component the run found to have moved in one subject. */\nexport interface Moved {\n readonly subject: string;\n readonly component: string;\n /**\n * Which bands moved, when the run could tell.\n *\n * Empty when the comparison produced names only — a baseline written before\n * the bands were split carries no per-band digests, and an empty list here\n * means *not known*, never *no band*.\n */\n readonly bands: readonly Band[];\n}\n\n/** What a run can put beside a movement to explain it. */\nexport interface Evidence {\n /**\n * Files in the change set, from whichever flag read a diff. Absent when none did.\n *\n * Absent and empty are different answers and are kept different: a run with no\n * `--since` has not established that nobody edited anything, and reading its\n * silence as \"no edits\" would attribute every movement in the suite to nothing\n * and call the result a flake list.\n */\n readonly changed?: readonly string[];\n\n /** Component name → the files that declare it, from the source index. */\n readonly declaredIn?: ReadonlyMap<string, readonly string[]>;\n\n /** Tokens whose resolved value moved in this run. Absent when nothing asked. */\n readonly tokens?: readonly string[];\n\n /** Subjects that failed to read the same way twice in this run. */\n readonly unstable?: ReadonlySet<string>;\n\n /**\n * Every component whose hashes moved, by subject — the whole reading, not the\n * causes.\n *\n * The control group is read from this and not from the movements, because the\n * movements are a *filtered* set: they come from regions the pixels named as\n * causes, so a component whose hashes moved and whose box no region named is\n * absent from them. Filtering the control group by the movements alone hands\n * back exactly those renders as *held still*, which is the one claim that\n * makes a control group worse than no control group.\n *\n * **Absent is not empty**, twice over. An absent map is a run that never\n * compared hashes, and the control group falls back to the movements with a\n * weaker guarantee. A subject *missing from* a present map had no hashes read\n * there — nobody measured it, so it cannot be a control for anything.\n */\n readonly hashesMoved?: ReadonlyMap<string, ReadonlySet<string>>;\n}\n\nexport interface Movement {\n readonly subject: string;\n readonly component: string;\n readonly bands: readonly Band[];\n readonly cause: Cause;\n /** One sentence, naming the evidence rather than the category. */\n readonly because: string;\n\n readonly file?: string;\n readonly tokens?: readonly string[];\n readonly upstream?: string;\n\n /**\n * Components between `upstream` and this one, outermost first.\n *\n * Empty when the edited component draws it directly, and present only on the\n * `upstream` rung. A reviewer sent to `ProductCard` for a `CardFooter` that\n * moved has one question — *how does that reach this* — and the answer is two\n * names the run already holds.\n */\n readonly through?: readonly string[];\n\n /**\n * Other subjects in which this same component moved in this run.\n *\n * The \"one cause, N subjects\" fold. A reviewer reading eleven changed subjects\n * is often reading one edit, and the count is the difference between a\n * frightening report and an accurate one.\n */\n readonly alsoIn: readonly string[];\n\n /**\n * Sites of this component, with the same props, whose hashes this run read and\n * found unmoved.\n *\n * The control group, and the reason an unexplained movement is worth\n * reporting rather than shrugging at. Empty means there was no control — the\n * component appears nowhere else with these inputs, or it moved in every one\n * of them — which weakens the finding and is why it is a list rather than a\n * flag.\n */\n readonly held: readonly Site[];\n\n /**\n * Sites of this component with the same props, other than this one, that the\n * run read — the pool `held` was drawn from.\n *\n * The denominator, and without it `held: []` is two opposite findings under one\n * shape. Zero is *nothing to compare against*; four with an empty `held` is a\n * comparison that was made, in four renders, and came back the same way in all\n * of them — the statement that nothing rendered under those props escaped\n * whatever moved here.\n */\n readonly compared: number;\n}\n\nexport interface Attribution {\n /** Every movement supplied, in the order supplied, each with a cause. */\n readonly movements: readonly Movement[];\n\n /**\n * Unexplained movements in subjects that also failed to read the same way\n * twice. A movement nothing explains, in a subject already proven unstable.\n */\n readonly flakes: readonly Movement[];\n\n /**\n * Unexplained movements in subjects nothing has read twice.\n *\n * Not a verdict. The shortlist a sweep should spend its second readings on,\n * ordered by how much control the suite has over each — a component that\n * rendered identically in six other places and moved here is a better use of a\n * second reading than one that appears nowhere else.\n */\n readonly suspects: readonly Movement[];\n}\n\n/**\n * Attribute every movement, given whatever evidence the run assembled.\n *\n * Degrades honestly. With no change set the `edited` rung is unreachable and\n * every movement falls to a lower one, so a run that did not ask cannot produce\n * a confident `unexplained` — which is checked here rather than trusted to the\n * caller, because the shortlist this feeds is exactly the thing somebody will\n * act on.\n */\nexport function attributeMovement(\n moved: readonly Moved[],\n composition: Composition,\n evidence: Evidence = {},\n): Attribution {\n const byComponent = new Map(composition.components.map((entry) => [entry.component, entry]));\n const movedIn = new Map<string, string[]>();\n for (const each of moved) {\n const subjects = movedIn.get(each.component) ?? [];\n subjects.push(each.subject);\n movedIn.set(each.component, subjects);\n }\n\n const edited = new Set(\n [...(evidence.declaredIn ?? new Map<string, readonly string[]>())]\n .filter(([, files]) => files.some((file) => (evidence.changed ?? []).includes(file)))\n .map(([component]) => component),\n );\n\n const graph = graphOf(composition);\n\n const movements = moved.map((each) =>\n attributeOne(each, byComponent.get(each.component), {\n movedIn,\n edited,\n evidence,\n composition,\n graph,\n }),\n );\n\n const unexplained = movements.filter((movement) => movement.cause === 'unexplained');\n\n return {\n movements,\n flakes: unexplained.filter((movement) => evidence.unstable?.has(movement.subject) === true),\n suspects: unexplained\n .filter((movement) => evidence.unstable?.has(movement.subject) !== true)\n .sort((a, b) => b.held.length - a.held.length),\n };\n}\n\n/** Everything the run assembled, gathered so one movement can be asked about. */\nexport interface Bench {\n readonly movedIn: ReadonlyMap<string, readonly string[]>;\n readonly edited: ReadonlySet<string>;\n readonly evidence: Evidence;\n readonly composition: Composition;\n readonly graph: Graph;\n}\n\nfunction attributeOne(moved: Moved, entry: ComponentEntry | undefined, bench: Bench): Movement {\n const { movedIn, edited, evidence, composition, graph } = bench;\n const alsoIn = (movedIn.get(moved.component) ?? []).filter(\n (subject) => subject !== moved.subject,\n );\n const { held, considered } = heldSites(moved, entry, bench);\n const base = { ...moved, alsoIn, held, compared: considered };\n\n const file = (evidence.declaredIn?.get(moved.component) ?? []).find((each) =>\n (evidence.changed ?? []).includes(each),\n );\n if (file !== undefined) {\n return { ...base, cause: 'edited', file, because: `\\`${file}\\` is in the change set` };\n }\n\n const tokens = movedTokens(moved.component, composition, evidence);\n if (tokens.length > 0) {\n return {\n ...base,\n cause: 'token',\n tokens,\n because: `it resolves through ${tokens.map((token) => `\\`${token}\\``).join(', ')}, which moved in this run`,\n };\n }\n\n const ancestor = editedAncestor(moved, edited, graph);\n if (ancestor !== undefined) {\n return {\n ...base,\n cause: 'upstream',\n upstream: ancestor.name,\n ...(ancestor.through.length === 0 ? {} : { through: ancestor.through }),\n because: becauseUpstream(ancestor),\n };\n }\n\n const contradiction = composition.divergences.find(\n (divergence) =>\n divergence.component === moved.component &&\n divergence.renderings.some((rendering) =>\n rendering.sites.some((site) => site.subject === moved.subject),\n ),\n );\n if (contradiction !== undefined) {\n return {\n ...base,\n cause: 'contradicted',\n because:\n `at this commit it renders ${contradiction.renderings.length} different ways from the same props ` +\n `(${contradiction.bands.join(', ')}), so the change is not in its own code`,\n };\n }\n\n return { ...base, cause: 'unexplained', because: unexplainedBecause(base, considered, evidence) };\n}\n\n/**\n * Who encloses what and who wrote what, in each subject separately.\n *\n * Keyed on the pair because the constraint is the whole point. `ComponentEntry.within`\n * and `ComponentEntry.createdBy` are both folded over the suite, so `Card` is enclosed by\n * `CartCard` and by `ProductCard` at once, and a walk that reads either hands a\n * reviewer the cart as the reason a product story moved. A site knows which\n * subject it was read in, so the graph can be built the way the question is\n * asked — and it has to be built that way for *both* edges, because the creator\n * is consulted first and an unconstrained answer there is never reached by the\n * constrained walk underneath it.\n */\ntype Enclosures = ReadonlyMap<string, readonly string[]>;\n\ninterface Graph {\n /** Who this component sits inside, per subject. */\n readonly within: Enclosures;\n /** Whose JSX wrote this component's element, per subject. */\n readonly wrote: Enclosures;\n}\n\nconst IN = '\\u0000';\n\nfunction graphOf(composition: Composition): Graph {\n const within = new Map<string, string[]>();\n const wrote = new Map<string, string[]>();\n\n const add = (graph: Map<string, string[]>, key: string, name: string): void => {\n const names = graph.get(key);\n if (names === undefined) graph.set(key, [name]);\n else if (!names.includes(name)) names.push(name);\n };\n\n for (const entry of composition.components) {\n for (const group of entry.classes) {\n for (const rendering of group.renderings) {\n for (const site of rendering.sites) {\n const key = `${site.subject}${IN}${entry.component}`;\n if (site.within !== undefined) add(within, key, site.within);\n if (site.createdBy !== undefined) add(wrote, key, site.createdBy);\n }\n }\n }\n }\n\n for (const names of within.values()) names.sort();\n for (const names of wrote.values()) names.sort();\n return { within, wrote };\n}\n\n/** An edited component above a moved one, and the components in between. */\nexport interface Ancestor {\n readonly name: string;\n /** Outermost first, and empty when the edited component draws it directly. */\n readonly through: readonly string[];\n}\n\n/**\n * The nearest edited component above this one, in the subject it moved in.\n *\n * The creator first, and it is not a tie-break. The component that *wrote the\n * element* is the one whose edit changed this component's inputs; the one it\n * happens to sit inside may be a presentational wrapper that knows nothing about\n * it. On `examples/todomvc` every `Chip` sits within a `Stack` and is created by\n * `TodoFooter`, so a run consulting only enclosure would fail to connect an edit\n * to `TodoFooter` with the chips it moved — and report five unexplained movements\n * instead of one caller. It is absent on a production build, which is why the\n * enclosure walk is not a fallback but the other half.\n *\n * Read off the per-subject graph and not off the entry. The entry's `createdBy`\n * is the suite's whole set, so an edit to whoever writes this component on one\n * page answers for every page it appears on — the same unconstrained answer the\n * walk below refuses, arriving one line earlier and winning.\n *\n * And that walk climbs rather than looking once. The rung has always said\n * *ancestor* and checked a parent, and a React tree is mostly components that\n * draw one wrapper each: `ProductCard` was edited, `ProductCard` draws `Card`,\n * `Card` draws `CardFooter`, and one look up arrives at `Card`, whose file nobody\n * touched. That is the shape of every unexplained movement this ladder used to\n * produce over an edit it was holding the graph for.\n */\nfunction editedAncestor(\n moved: Moved,\n edited: ReadonlySet<string>,\n graph: Graph,\n): Ancestor | undefined {\n const here = `${moved.subject}${IN}${moved.component}`;\n const wrote = (graph.wrote.get(here) ?? []).find((name) => edited.has(name));\n if (wrote !== undefined) return { name: wrote, through: [] };\n\n // Breadth-first, so the answer is the *nearest* edit and not whichever one the\n // recursion reached first. `seen` is written as a rung is built: an enclosure\n // graph folded over a suite can close a loop, and a component reached twice on\n // one rung would otherwise queue twice.\n const seen = new Set([moved.component]);\n const step = (from: Ancestor | null): Ancestor[] => {\n const child = from === null ? moved.component : from.name;\n const rung: Ancestor[] = [];\n for (const holder of graph.within.get(`${moved.subject}${IN}${child}`) ?? []) {\n if (seen.has(holder)) continue;\n seen.add(holder);\n rung.push({ name: holder, through: from === null ? [] : [from.name, ...from.through] });\n }\n return rung;\n };\n\n let rung = step(null);\n while (rung.length > 0) {\n const arrived = rung.find((each) => edited.has(each.name));\n if (arrived !== undefined) return arrived;\n rung = rung.flatMap(step);\n }\n\n return undefined;\n}\n\n/**\n * Tokens this component resolves through that moved in this run.\n *\n * Read off the instances rather than off the subject, which is the distinction\n * that makes the answer worth anything: every subject on a themed page resolves\n * through every token in the theme, so a subject-level intersection names them\n * all and explains nothing.\n */\nfunction movedTokens(\n component: string,\n composition: Composition,\n evidence: Evidence,\n): readonly string[] {\n if (evidence.tokens === undefined || evidence.tokens.length === 0) return [];\n\n const entry = composition.components.find((each) => each.component === component);\n if (entry === undefined) return [];\n\n const moved = new Set(evidence.tokens);\n return [...entry.tokens].filter((token) => moved.has(token)).sort();\n}\n"]}
@@ -135,6 +135,49 @@ export interface AttributionOptions {
135
135
  * is named rather than buried.
136
136
  */
137
137
  readonly containment?: number;
138
+ /**
139
+ * The snapshot of the render the pixels are compared against.
140
+ *
141
+ * Required, and `undefined` is an answer: it says the other render is not
142
+ * known. A changed region is the union of what left and what arrived, so each
143
+ * node is joined against its box in this render and the box of the node the
144
+ * differ's matcher pairs it with in that one. Without it a control replaced
145
+ * by a larger element changes pixels outside either box alone, and the join
146
+ * names the row it sits in.
147
+ *
148
+ * Its boxes are placed by where they were in its own image, whose corner is
149
+ * {@link beforeOrigin}. A shot clipped to the subject starts at its root, so
150
+ * a root the page moved between the two shots moves none of the pixels, and
151
+ * each box that left is shifted by it.
152
+ */
153
+ readonly before: SemanticSnapshot | undefined;
154
+ /**
155
+ * Page-space CSS origin of the before image's top-left corner.
156
+ *
157
+ * Defaults to `before`'s own root rect, as `origin` defaults to this
158
+ * render's. Required when `origin` is stated and `before` is a snapshot: two
159
+ * shots clipped to anything but the root share a corner only by the page's
160
+ * layout, and a corner guessed from the other image gives a plausible report
161
+ * about the wrong components. A full-page pair passes `{x: 0, y: 0}` for both.
162
+ */
163
+ readonly beforeOrigin?: {
164
+ readonly x: number;
165
+ readonly y: number;
166
+ };
167
+ /**
168
+ * The rows of this image that have no partner in the before image, as
169
+ * half-open runs of raster rows.
170
+ *
171
+ * Such a row is marked edge to edge, because nothing says where its content
172
+ * begins, so a region inside one is as wide as the image and only the root
173
+ * contains it by area. It is joined by its rows instead: to the tightest box
174
+ * whose rows span it, which is the box that grew. Absent when the rows were
175
+ * compared by position.
176
+ */
177
+ readonly inserted?: readonly {
178
+ readonly from: number;
179
+ readonly to: number;
180
+ }[];
138
181
  }
139
182
  /**
140
183
  * Join isolated regions to the nodes that occupy them.
@@ -156,13 +199,13 @@ export interface RankedRegion extends AttributedRegion {
156
199
  *
157
200
  * This function exists because of a measurement, and the measurement is the most
158
201
  * useful thing the raster tier has produced. Replacing a checkbox with a styled
159
- * div on the todomvc corpus changes 1530 pixels in 5 regions, which attribute
202
+ * div on the todomvc corpus changes 1853 pixels in 5 regions, which attribute
160
203
  * geometrically and rank by area as:
161
204
  *
162
205
  * ```
163
- * 933px Text <- changed, and reflowed
164
- * 511px Stack <- only reflowed
165
- * 86px Toggle <- the edit
206
+ * 1055px Text <- changed, and reflowed
207
+ * 614px Stack <- only reflowed
208
+ * 184px Toggle <- the edit
166
209
  * ```
167
210
  *
168
211
  * Every one of those attributions is correct. The pixels really are inside those
@@ -1,4 +1,6 @@
1
+ import { intersectionArea } from '../format/capture.js';
1
2
  import { locate } from './locate.js';
3
+ import { matchTrees } from '../compare/diff/match.js';
2
4
  const DEFAULT_CONTAINMENT = 0.9;
3
5
  /**
4
6
  * Join isolated regions to the nodes that occupy them.
@@ -8,8 +10,19 @@ const DEFAULT_CONTAINMENT = 0.9;
8
10
  * correct answer — a rect that was never observed must never be inferred.
9
11
  */
10
12
  export function attributeRegions(regions, snapshot, options) {
11
- const boxed = collectBoxes(snapshot.root);
13
+ if (options.origin !== undefined && options.before !== undefined && options.beforeOrigin === undefined) {
14
+ throw new Error('`origin` is stated and `beforeOrigin` is not: state the corner of the before image too, ' +
15
+ 'since two shots share one only when the page did not move between them');
16
+ }
12
17
  const origin = options.origin ?? snapshot.root.rect ?? { x: 0, y: 0 };
18
+ // A box that left is placed by where it was in its image, not on the page.
19
+ const was = options.beforeOrigin ?? options.before?.root.rect ?? origin;
20
+ const left = keptBoxes(options.before, snapshot, { x: origin.x - was.x, y: origin.y - was.y });
21
+ const boxed = collectBoxes(snapshot.root).map((node) => {
22
+ const before = left.get(node);
23
+ const rects = before === undefined ? [node.rect] : [node.rect, before];
24
+ return { node, rects, area: extent(rects) };
25
+ });
13
26
  const containment = options.containment ?? DEFAULT_CONTAINMENT;
14
27
  return regions.map((region) => {
15
28
  const css = {
@@ -19,14 +32,16 @@ export function attributeRegions(regions, snapshot, options) {
19
32
  height: region.height / options.scale,
20
33
  };
21
34
  const area = css.width * css.height;
35
+ // Rows the after image added are joined by the rows they span, and only to
36
+ // this render's box: the before side never painted them.
37
+ const added = options.inserted?.some((run) => run.from <= region.y && region.y + region.height <= run.to) ?? false;
22
38
  let inside;
23
39
  let overlapping;
24
- for (const node of boxed) {
25
- const rect = node.rect;
26
- const shared = intersectionArea(css, rect);
40
+ for (const box of boxed) {
41
+ const shared = covered(css, box.rects);
27
42
  if (shared <= 0)
28
43
  continue;
29
- const nodeArea = rect.width * rect.height;
44
+ const nodeArea = box.area;
30
45
  // `<=`, not `<`, and the difference is one component name in every report
31
46
  // over a design system. A wrapper that shrink-wraps its only child carries
32
47
  // a *byte-identical* rect — measured on `cases/storybook-case`, where
@@ -36,21 +51,24 @@ export function attributeRegions(regions, snapshot, options) {
36
51
  // one: the run then names the wrapper nothing edited and sends a reviewer
37
52
  // to its file. Ties go to the last box seen, which is the innermost, and is
38
53
  // also the one the browser painted on top.
39
- if (area > 0 && shared / area >= containment) {
40
- if (inside === undefined || nodeArea <= inside.rect.width * inside.rect.height)
41
- inside = node;
54
+ const contained = added
55
+ ? spans(box.node.rect, css) && box.node.rect.width * box.node.rect.height > 0
56
+ : area > 0 && shared / area >= containment;
57
+ if (contained) {
58
+ if (inside === undefined || nodeArea <= inside.area)
59
+ inside = box;
42
60
  }
43
61
  // The same rule for `nearest`. It is orientation rather than attribution,
44
62
  // and a hint should point at the same node the join would have picked.
45
- if (overlapping === undefined || nodeArea <= overlapping.rect.width * overlapping.rect.height) {
46
- overlapping = node;
63
+ if (overlapping === undefined || nodeArea <= overlapping.area) {
64
+ overlapping = box;
47
65
  }
48
66
  }
49
67
  if (inside !== undefined) {
50
68
  return {
51
69
  region,
52
- path: inside.path,
53
- ...describe(snapshot, inside),
70
+ path: inside.node.path,
71
+ ...describe(snapshot, inside.node),
54
72
  unattributed: false,
55
73
  };
56
74
  }
@@ -58,7 +76,7 @@ export function attributeRegions(regions, snapshot, options) {
58
76
  region,
59
77
  unattributed: true,
60
78
  ...(overlapping !== undefined
61
- ? { nearest: { path: overlapping.path, ...describe(snapshot, overlapping) } }
79
+ ? { nearest: { path: overlapping.node.path, ...describe(snapshot, overlapping.node) } }
62
80
  : {}),
63
81
  };
64
82
  });
@@ -68,13 +86,13 @@ export function attributeRegions(regions, snapshot, options) {
68
86
  *
69
87
  * This function exists because of a measurement, and the measurement is the most
70
88
  * useful thing the raster tier has produced. Replacing a checkbox with a styled
71
- * div on the todomvc corpus changes 1530 pixels in 5 regions, which attribute
89
+ * div on the todomvc corpus changes 1853 pixels in 5 regions, which attribute
72
90
  * geometrically and rank by area as:
73
91
  *
74
92
  * ```
75
- * 933px Text <- changed, and reflowed
76
- * 511px Stack <- only reflowed
77
- * 86px Toggle <- the edit
93
+ * 1055px Text <- changed, and reflowed
94
+ * 614px Stack <- only reflowed
95
+ * 184px Toggle <- the edit
78
96
  * ```
79
97
  *
80
98
  * Every one of those attributions is correct. The pixels really are inside those
@@ -124,6 +142,62 @@ function describe(snapshot, node) {
124
142
  ...(node.provenance?.source ? { source: node.provenance.source } : {}),
125
143
  };
126
144
  }
145
+ /**
146
+ * The box each node in `after` had in `before`, by the node.
147
+ *
148
+ * Paired by `matchTrees`, the matcher the semantic diff reads, so a region and
149
+ * a delta never disagree about which element a node was. A path is not the key
150
+ * across renders because it is a position, and a sibling inserted ahead
151
+ * renumbers the node. A box with no area paints nothing, so it widens nothing.
152
+ */
153
+ function keptBoxes(before, after, shift) {
154
+ const kept = new Map();
155
+ if (before === undefined)
156
+ return kept;
157
+ for (const [left, arrived] of matchTrees(before.root, after.root).pairs) {
158
+ const rect = left.rect;
159
+ if (rect !== undefined && rect.width * rect.height > 0) {
160
+ kept.set(arrived, { ...rect, x: rect.x + shift.x, y: rect.y + shift.y });
161
+ }
162
+ }
163
+ return kept;
164
+ }
165
+ /**
166
+ * How much of `rect` the boxes cover, counting what two of them share once.
167
+ *
168
+ * A node's two boxes are two places, not the rectangle that holds both: a node
169
+ * that moved across a row was never in the space between, and a change there is
170
+ * not its.
171
+ */
172
+ function covered(rect, [first, second]) {
173
+ const one = intersectionArea(rect, first);
174
+ if (second === undefined)
175
+ return one;
176
+ const both = overlap(first, second);
177
+ return one + intersectionArea(rect, second) - (both === undefined ? 0 : intersectionArea(rect, both));
178
+ }
179
+ /** Whether `box`'s rows cover every row of `rect`, wherever their columns are. */
180
+ function spans(box, rect) {
181
+ return box.y <= rect.y && rect.y + rect.height <= box.y + box.height;
182
+ }
183
+ /** The area the boxes cover together, counting what two of them share once. */
184
+ function extent(rects) {
185
+ const [first, second] = rects;
186
+ if (second === undefined)
187
+ return first.width * first.height;
188
+ const x = Math.min(first.x, second.x);
189
+ const y = Math.min(first.y, second.y);
190
+ const width = Math.max(first.x + first.width, second.x + second.width) - x;
191
+ const height = Math.max(first.y + first.height, second.y + second.height) - y;
192
+ return covered({ x, y, width, height }, rects);
193
+ }
194
+ function overlap(a, b) {
195
+ const x = Math.max(a.x, b.x);
196
+ const y = Math.max(a.y, b.y);
197
+ const width = Math.min(a.x + a.width, b.x + b.width) - x;
198
+ const height = Math.min(a.y + a.height, b.y + b.height) - y;
199
+ return width > 0 && height > 0 ? { x, y, width, height } : undefined;
200
+ }
127
201
  /** Every node carrying a rect, in document order. Zero-area boxes are skipped. */
128
202
  function collectBoxes(root) {
129
203
  const out = [];
@@ -136,9 +210,4 @@ function collectBoxes(root) {
136
210
  walk(root);
137
211
  return out;
138
212
  }
139
- function intersectionArea(a, b) {
140
- const width = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
141
- const height = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
142
- return width > 0 && height > 0 ? width * height : 0;
143
- }
144
213
  //# sourceMappingURL=region.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"region.js","sourceRoot":"","sources":["../../src/attribute/region.ts"],"names":[],"mappings":"AACA,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;AAED,SAAS,gBAAgB,CAAC,CAAO,EAAE,CAAO;IACxC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,OAAO,KAAK,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACtD,CAAC","sourcesContent":["import 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\nfunction intersectionArea(a: Rect, b: Rect): number {\n const width = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);\n const height = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);\n return width > 0 && height > 0 ? width * height : 0;\n}\n"]}
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"]}
@@ -21,8 +21,10 @@
21
21
  * configuring a build, and it answers with where a component is *declared* —
22
22
  * coarser than a call site, and enough to open the right file.
23
23
  *
24
- * The index is plain data. `core` performs no I/O (ADR-0006), so building one is
25
- * a caller's job; resolving against one is here.
24
+ * The index is plain data. `core` performs no I/O (ADR-0006) and has no parser,
25
+ * so building one is a caller's job — `indexDeclarations` in
26
+ * [`@variance-authority/sense`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/sense/src/declarations.ts) reads it off
27
+ * a module's parse — and resolving against one is here.
26
28
  */
27
29
  export interface SourceRef {
28
30
  /** Repository-relative, so a report is portable between machines and CI. */
@@ -31,12 +33,13 @@ export interface SourceRef {
31
33
  /**
32
34
  * How the declaration was recognised. Carried so a bad match is debuggable.
33
35
  *
34
- * The first four are shapes a scan of the source text matched by name.
35
- * `engine` is the one that is not a match at all: the browser was asked where
36
- * the function it rendered was compiled from, and that position was mapped
37
- * back to the file. It names the component that ran, whatever it was called.
36
+ * The first three say which kind of statement declared the name; a `let`
37
+ * binding is shown as `const`. `engine` is the one that is not read from
38
+ * source at all: the browser was asked where the function it rendered was
39
+ * compiled from, and that position was mapped back to the file. It names the
40
+ * component that ran, whatever it was called.
38
41
  */
39
- readonly via: 'function' | 'const' | 'class' | 'declared' | 'engine';
42
+ readonly via: 'function' | 'const' | 'class' | 'engine';
40
43
  }
41
44
  /**
42
45
  * Component name → where it is declared.
@@ -62,23 +65,6 @@ export declare function resolveSource(name: string, index: SourceIndex): Resolut
62
65
  * every agent harness already know how to open.
63
66
  */
64
67
  export declare function formatSource(resolution: Resolution): string;
65
- /**
66
- * Extract component declarations from one file's source.
67
- *
68
- * A regex scan, and the limits are worth stating rather than discovering. It
69
- * finds exported and local declarations in the three shapes React components are
70
- * written in, and it will miss a component produced by a factory, assigned
71
- * dynamically, or re-exported under another name. It can also match a function
72
- * that merely looks like a component — capitalised, declared at top level — and
73
- * name a non-component in a report.
74
- *
75
- * Both failures are survivable in a way a wrong file path would not be: a miss
76
- * degrades the report to the component name, which is what it said before, and a
77
- * false positive can only appear if attribution already named that identifier.
78
- * Parsing properly is the right answer eventually; it is not worth a parser
79
- * dependency to find out whether the link is useful.
80
- */
81
- export declare function indexSource(file: string, contents: string): SourceIndex;
82
68
  /** Merge per-file indexes. A name declared in several files keeps every ref. */
83
69
  export declare function mergeSourceIndexes(indexes: readonly SourceIndex[]): SourceIndex;
84
70
  /**