@variance-authority/core 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +245 -10
  2. package/README.md +279 -110
  3. package/dist/attribute/call-site.d.ts +20 -2
  4. package/dist/attribute/call-site.js +17 -20
  5. package/dist/attribute/call-site.js.map +1 -1
  6. package/dist/attribute/closure.d.ts +105 -0
  7. package/dist/attribute/closure.js +180 -0
  8. package/dist/attribute/closure.js.map +1 -0
  9. package/dist/attribute/composition.d.ts +5 -3
  10. package/dist/attribute/composition.js +67 -11
  11. package/dist/attribute/composition.js.map +1 -1
  12. package/dist/attribute/divergence.js +7 -1
  13. package/dist/attribute/divergence.js.map +1 -1
  14. package/dist/attribute/index.d.ts +7 -1
  15. package/dist/attribute/index.js +4 -1
  16. package/dist/attribute/index.js.map +1 -1
  17. package/dist/attribute/landmark.d.ts +97 -0
  18. package/dist/attribute/landmark.js +144 -0
  19. package/dist/attribute/landmark.js.map +1 -0
  20. package/dist/attribute/lexicon.d.ts +112 -0
  21. package/dist/attribute/lexicon.js +210 -0
  22. package/dist/attribute/lexicon.js.map +1 -0
  23. package/dist/attribute/source.d.ts +19 -2
  24. package/dist/attribute/source.js +12 -0
  25. package/dist/attribute/source.js.map +1 -1
  26. package/dist/attribute/stack.d.ts +62 -1
  27. package/dist/attribute/stack.js +116 -5
  28. package/dist/attribute/stack.js.map +1 -1
  29. package/dist/compare/derive.d.ts +17 -0
  30. package/dist/compare/derive.js.map +1 -1
  31. package/dist/compare/diff/index.d.ts +25 -1
  32. package/dist/compare/diff/index.js +8 -5
  33. package/dist/compare/diff/index.js.map +1 -1
  34. package/dist/compare/explain.d.ts +1 -1
  35. package/dist/compare/explain.js +15 -8
  36. package/dist/compare/explain.js.map +1 -1
  37. package/dist/compare/index.d.ts +2 -2
  38. package/dist/compare/index.js +1 -1
  39. package/dist/compare/index.js.map +1 -1
  40. package/dist/compare/observability.d.ts +25 -1
  41. package/dist/compare/observability.js +54 -0
  42. package/dist/compare/observability.js.map +1 -1
  43. package/dist/compare/parting.d.ts +24 -2
  44. package/dist/compare/parting.js +14 -9
  45. package/dist/compare/parting.js.map +1 -1
  46. package/dist/compare/slice.d.ts +15 -2
  47. package/dist/compare/slice.js +6 -2
  48. package/dist/compare/slice.js.map +1 -1
  49. package/dist/format/capture.d.ts +15 -0
  50. package/dist/format/capture.js +30 -1
  51. package/dist/format/capture.js.map +1 -1
  52. package/dist/format/document.d.ts +65 -5
  53. package/dist/format/document.js +34 -0
  54. package/dist/format/document.js.map +1 -1
  55. package/dist/format/hash.d.ts +26 -0
  56. package/dist/format/hash.js +41 -0
  57. package/dist/format/hash.js.map +1 -1
  58. package/dist/format/index.d.ts +4 -3
  59. package/dist/format/index.js +3 -2
  60. package/dist/format/index.js.map +1 -1
  61. package/dist/format/sha256.d.ts +10 -2
  62. package/dist/format/sha256.js +42 -9
  63. package/dist/format/sha256.js.map +1 -1
  64. package/dist/format/stabilize.d.ts +15 -0
  65. package/dist/format/stabilize.js +22 -1
  66. package/dist/format/stabilize.js.map +1 -1
  67. package/dist/index.d.ts +3 -11
  68. package/dist/index.js +3 -11
  69. package/dist/index.js.map +1 -1
  70. package/dist/judge/inspect.js +57 -4
  71. package/dist/judge/inspect.js.map +1 -1
  72. package/dist/relate/before.d.ts +137 -0
  73. package/dist/relate/before.js +155 -0
  74. package/dist/relate/before.js.map +1 -0
  75. package/dist/relate/graph.d.ts +53 -16
  76. package/dist/relate/graph.js +33 -15
  77. package/dist/relate/graph.js.map +1 -1
  78. package/dist/relate/index.d.ts +3 -2
  79. package/dist/relate/index.js +2 -1
  80. package/dist/relate/index.js.map +1 -1
  81. package/dist/relate/merkle.d.ts +14 -5
  82. package/dist/relate/merkle.js +14 -6
  83. package/dist/relate/merkle.js.map +1 -1
  84. package/dist/relate/reach.d.ts +19 -5
  85. package/dist/relate/reach.js +10 -8
  86. package/dist/relate/reach.js.map +1 -1
  87. package/dist/relate/records.d.ts +95 -4
  88. package/dist/relate/records.js +93 -13
  89. package/dist/relate/records.js.map +1 -1
  90. package/dist/segment/index.d.ts +123 -0
  91. package/dist/segment/index.js +265 -0
  92. package/dist/segment/index.js.map +1 -0
  93. package/dist/share/index.d.ts +148 -0
  94. package/dist/share/index.js +190 -0
  95. package/dist/share/index.js.map +1 -0
  96. package/package.json +10 -2
@@ -1 +1 @@
1
- {"version":3,"file":"composition.js","sourceRoot":"","sources":["../../src/attribute/composition.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticSnapshot } from '../format/snapshot.js';\nimport { divergencesOf, type Divergence } from './divergence.js';\nimport { attributed, type ComponentInstance } from './instances.js';\n\n/**\n * The suite read as one graph, at one commit.\n *\n * Every other comparison in this system is between two revisions of one subject.\n * This is the other axis: **many subjects, one revision**, joined on the\n * components they share. It answers the question a per-subject tool cannot even\n * phrase — *this component, here, is the same component you already have an\n * example of over there* — and it answers it from digests a run already\n * computed, with no second render, no image and no store.\n *\n * ## What the join is worth\n *\n * A suite of examples is a suite of *compositions*. The narrow example is a\n * component at a boundary; the page examples contain that same boundary again,\n * sometimes with the same props and sometimes not. Once the instances are\n * addressable, three facts fall out that nobody had:\n *\n * - **An echo.** One rendering, several subjects. The narrow example and the\n * page are watching literally the same bytes, so a change in one is a change\n * in all of them, and a reviewer looking at eleven diffs is looking at one.\n * - **A divergence.** Same component, same props, *different* rendering, at one\n * commit. Nothing about the component's own inputs can explain that. Either\n * something outside it decides its output — an ancestor's cascade, a token, its\n * own state — or the reading is not repeatable. Both are findings and neither\n * is a regression.\n * - **A control group.** For any instance that moved, the same rendering\n * somewhere else that did *not* is a stable state to refer to. That is the\n * referent flake attribution has always needed and never had, and the suite\n * supplies it for free: one commit already holds N renderings of every shared\n * component.\n *\n * ## What it deliberately does not do\n *\n * It never decides anything. Two subjects sharing a rendering is not a reason to\n * delete either — that is the argument `coverage.ts` makes at the level of names\n * and it holds harder here, because a component can be correct in one context\n * and broken in the next, which is why the contexts are separate subjects.\n */\n\n/** One instance, located. */\nexport interface Site {\n readonly subject: string;\n readonly path: NodePath;\n readonly depth: number;\n readonly within?: string;\n\n /**\n * The component whose JSX wrote this element, here.\n *\n * On the entry beside it, `createdBy` is a set folded over the whole suite —\n * every component that ever wrote one of these, anywhere. That is the right\n * shape for *who mounts this* and the wrong one for *who mounted this here*,\n * and the ladder asks the second: an edit to a component that writes a `Chip`\n * on the footer explains nothing about a `Chip` the sidebar wrote. Kept per\n * site so the question can be asked where it was answered.\n */\n readonly createdBy?: string;\n}\n\n/** One rendering of one component, and everywhere it occurred. */\nexport interface Rendering {\n readonly rendering: Digest;\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n\n /**\n * Child components this rendering mounted, in document order.\n *\n * Carried on the rendering rather than only on the entry because it is the\n * only evidence available about what was passed *in*: `propsDigest` excludes\n * `children` by design, so two renderings that mount different children are\n * two different inputs wearing one props digest. `divergencesOf` refuses to\n * call that a contradiction, and this is the field it reads.\n */\n readonly renders: readonly string[];\n\n /** In subject order, then document order. */\n readonly sites: readonly Site[];\n}\n\n/** One props digest of one component, and every way it rendered under it. */\nexport interface PropsClass {\n /** Absent when the collector supplied no provenance — unknown, not \"none\". */\n readonly props?: Digest;\n readonly renderings: readonly Rendering[];\n}\n\nexport interface ComponentEntry {\n readonly component: string;\n /** Subjects containing at least one boundary of it, in the order supplied. */\n readonly subjects: readonly string[];\n /** Boundaries summed across every subject. Distinct from `subjects.length`. */\n readonly instances: number;\n\n /**\n * Subjects whose shallowest attributed boundary is this component.\n *\n * The narrow example — the subject that exists to show this thing, rather than\n * a page that happens to contain it. Defined by depth rather than by a naming\n * convention because a naming convention is a different tool's `story:` prefix,\n * and this has to hold for a route suite too. Ties resolve to nothing: a\n * subject whose two shallowest boundaries are siblings has no single subject.\n */\n readonly examples: readonly string[];\n\n /** Components that enclose it somewhere, sorted. The graph, upwards. */\n readonly within: readonly string[];\n\n /**\n * Components that *mounted* it somewhere, sorted. The graph, upwards, again —\n * and usually the more useful of the two.\n *\n * `within` is where the boundary sits in the DOM and this is who wrote the\n * element. They are the same thing only for a component that authors a node of\n * its own, and a real application is full of components that do not: measured\n * on `examples/todomvc`, every `Chip` reports `within: ['Stack']` and\n * `createdBy: ['TodoFooter']`. `TodoFooter` renders nothing but other\n * components, so it owns no node, is a boundary nowhere, and appears in this\n * graph *only* here — while being the file a reviewer has to open.\n *\n * Empty on a production build, where `_debugOwner` is absent (ADR-0007). Empty\n * is *not* \"nothing mounted it\": a component with no caller is the subject root\n * and every other case is missing data, which is why the `upstream` rung in\n * `movement.ts` reads this and never concludes from its silence.\n */\n readonly createdBy: readonly string[];\n\n /** Components it encloses somewhere, sorted. The graph, downwards. */\n readonly renders: readonly string[];\n\n /**\n * Custom properties its own nodes resolve through, anywhere in the suite.\n *\n * The set a token movement is intersected against, which is how \"`--va-space-3`\n * moved\" becomes \"`--va-space-3` moved and these four components read it\".\n */\n readonly tokens: readonly string[];\n\n /** Sorted by props digest; the class with unknown props sorts last. */\n readonly classes: readonly PropsClass[];\n}\n\n/**\n * The same component, the same props, the same bytes, in more than one subject.\n *\n * The connected dots. `example` names the narrow subject among the sites when\n * there is one, because that is the artefact a reviewer already has an opinion\n * about — \"this is the Chip from the Chip story\" is a sentence, and \"these four\n * paths share a digest\" is not.\n */\nexport interface Echo {\n readonly component: string;\n readonly props?: Digest;\n readonly rendering: Digest;\n /** At least two, spanning at least two subjects. */\n readonly sites: readonly Site[];\n readonly example?: string;\n}\n\n\nexport interface SubjectComposition {\n readonly subject: string;\n readonly instances: readonly ComponentInstance[];\n\n /**\n * The subject as read, kept so a divergence can say *which input* moved.\n *\n * Optional because `composeSubjects` is a fold over instance lists and stays\n * one — a caller holding only a report's sidecars still gets the graph, the\n * echoes and the divergences, and gets them without a `partings` field it\n * would have to explain away as empty.\n */\n readonly snapshot?: SemanticSnapshot;\n}\n\nexport interface Composition {\n /** In the order supplied, which the run guarantees is plan order. */\n readonly subjects: readonly string[];\n /** Sorted by name, code-unit order. */\n readonly components: readonly ComponentEntry[];\n /** Sorted by how many sites each has, widest first, then by component. */\n readonly echoes: readonly Echo[];\n /** Sorted by component name. */\n readonly divergences: readonly Divergence[];\n}\n\n/**\n * Fold a run's per-subject instance lists into the graph.\n *\n * Pure, ordered, and a function of its input alone — the run's report has to be\n * a function of the plan, and a phase that accumulated as a worker pool finished\n * would produce a different artefact from the same suite on a slower machine.\n */\nexport function composeSubjects(subjects: readonly SubjectComposition[]): Composition {\n const entries = new Map<string, Accumulator>();\n const order = subjects.map((subject) => subject.subject);\n\n for (const subject of subjects) {\n const example = exampleOf(subject.instances);\n\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n\n const entry = accumulate(entries, instance.component);\n if (entry.subjects.at(-1) !== subject.subject) entry.subjects.push(subject.subject);\n entry.instances += 1;\n if (example === instance.component && entry.examples.at(-1) !== subject.subject) {\n entry.examples.push(subject.subject);\n }\n if (instance.within !== undefined) entry.within.add(instance.within);\n if (instance.createdBy !== undefined) entry.createdBy.add(instance.createdBy);\n for (const child of instance.renders) entry.renders.add(child);\n for (const token of instance.tokens) entry.tokens.add(token);\n\n const key = instance.props ?? UNKNOWN_PROPS;\n const byProps = entry.classes.get(key) ?? new Map<Digest, MutableRendering>();\n const rendering = byProps.get(instance.rendering) ?? {\n rendering: instance.rendering,\n structure: instance.structure,\n semantics: instance.semantics,\n text: instance.text,\n style: instance.style,\n renders: instance.renders,\n sites: [],\n };\n rendering.sites.push({\n subject: subject.subject,\n path: instance.path,\n depth: instance.depth,\n ...(instance.within === undefined ? {} : { within: instance.within }),\n ...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),\n });\n byProps.set(instance.rendering, rendering);\n entry.classes.set(key, byProps);\n }\n }\n\n const components = [...entries.entries()]\n .map(([component, entry]) => ({\n component,\n subjects: entry.subjects,\n instances: entry.instances,\n examples: entry.examples,\n within: [...entry.within].sort(byCodeUnit),\n createdBy: [...entry.createdBy].sort(byCodeUnit),\n renders: [...entry.renders].sort(byCodeUnit),\n tokens: [...entry.tokens].sort(byCodeUnit),\n classes: [...entry.classes.entries()]\n .sort(([a], [b]) => byCodeUnit(a, b))\n .map(([props, renderings]) => ({\n ...(props === UNKNOWN_PROPS ? {} : { props }),\n renderings: [...renderings.values()].sort(widestFirst),\n })),\n }))\n .sort((a, b) => byCodeUnit(a.component, b.component));\n\n const snapshots = new Map<string, SemanticSnapshot>();\n for (const subject of subjects) {\n if (subject.snapshot !== undefined) snapshots.set(subject.subject, subject.snapshot);\n }\n\n return {\n subjects: order,\n components,\n echoes: echoesOf(components),\n divergences: divergencesOf(components, snapshots),\n };\n}\n\n/**\n * Every rendering shared by two or more subjects.\n *\n * Two instances of one component inside a *single* subject sharing a rendering\n * is ordinary and says nothing — a list of three identical chips is three\n * identical chips. The finding is a rendering that survives being mounted\n * somewhere else, because that is the one a second subject is watching.\n */\nfunction echoesOf(components: readonly ComponentEntry[]): readonly Echo[] {\n const echoes: Echo[] = [];\n\n for (const entry of components) {\n for (const group of entry.classes) {\n for (const rendering of group.renderings) {\n const spread = new Set(rendering.sites.map((site) => site.subject));\n if (spread.size < 2) continue;\n\n const example = entry.examples.find((subject) => spread.has(subject));\n echoes.push({\n component: entry.component,\n ...(group.props === undefined ? {} : { props: group.props }),\n rendering: rendering.rendering,\n sites: rendering.sites,\n ...(example === undefined ? {} : { example }),\n });\n }\n }\n }\n\n return echoes.sort(\n (a, b) => b.sites.length - a.sites.length || byCodeUnit(a.component, b.component),\n );\n}\n\n\n/**\n * The component a subject exists to show, when one component does.\n *\n * The shallowest attributed boundary, and only when it is alone at that depth.\n * A story usually mounts one thing; a page mounts a layout that mounts several,\n * and calling the first of them the subject's component would be picking a\n * winner out of document order.\n */\nfunction exampleOf(instances: readonly ComponentInstance[]): string | undefined {\n const shallowest = instances.filter(attributed).reduce<number>((depth, instance) => {\n return Math.min(depth, instance.depth);\n }, Number.POSITIVE_INFINITY);\n\n const leads = instances.filter((instance) => attributed(instance) && instance.depth === shallowest);\n const names = new Set(leads.map((instance) => instance.component));\n return leads.length === 1 || names.size === 1 ? leads[0]?.component : undefined;\n}\n\n/**\n * The key an instance with no props digest is filed under.\n *\n * A sentinel rather than `undefined`, because a `Map` keyed on `Digest |\n * undefined` reads as though unknown props were a props class like any other,\n * and every consumer would then have to remember that it is not. It sorts last\n * by construction — no digest starts with a space.\n */\nconst UNKNOWN_PROPS = ' unknown';\n\ninterface Accumulator {\n readonly subjects: string[];\n instances: number;\n readonly examples: string[];\n readonly within: Set<string>;\n readonly createdBy: Set<string>;\n readonly renders: Set<string>;\n readonly tokens: Set<string>;\n readonly classes: Map<string, Map<Digest, MutableRendering>>;\n}\n\n/** A `Rendering` while its sites are still being collected. */\ninterface MutableRendering extends Omit<Rendering, 'sites'> {\n readonly sites: Site[];\n}\n\nfunction accumulate(entries: Map<string, Accumulator>, component: string): Accumulator {\n const existing = entries.get(component);\n if (existing !== undefined) return existing;\n\n const created: Accumulator = {\n subjects: [],\n instances: 0,\n examples: [],\n within: new Set(),\n createdBy: new Set(),\n renders: new Set(),\n tokens: new Set(),\n classes: new Map(),\n };\n entries.set(component, created);\n return created;\n}\n\nfunction widestFirst(a: Rendering, b: Rendering): number {\n return b.sites.length - a.sites.length || byCodeUnit(a.rendering, b.rendering);\n}\n\n/**\n * Code-unit order, never `localeCompare`.\n *\n * The same rule `hashComponents` sorts by, for the same reason: this output\n * reaches a report that is committed, diffed and read back on another runner,\n * and a locale-aware comparison makes the byte order a promise about `LANG`.\n */\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
1
+ {"version":3,"file":"composition.js","sourceRoot":"","sources":["../../src/attribute/composition.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAmB,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,UAAU,EAA0B,MAAM,gBAAgB,CAAC;AAgMpE;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,QAAuC;IACrE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,MAAM,UAAU,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IAElD,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QAEzD,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;YACzC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAEpC,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;YACtD,IAAI,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,OAAO;gBAAE,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACpF,KAAK,CAAC,SAAS,IAAI,CAAC,CAAC;YACrB,IAAI,OAAO,KAAK,QAAQ,CAAC,SAAS,IAAI,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC;gBAChF,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACvC,CAAC;YACD,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS;gBAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACrE,IAAI,QAAQ,CAAC,SAAS,KAAK,SAAS;gBAAE,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YAC9E,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,OAAO;gBAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC/D,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,MAAM;gBAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAE7D,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,IAAI,aAAa,CAAC;YAC5C,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,EAA4B,CAAC;YAC9E,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI;gBACnD,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,OAAO,EAAE,QAAQ,CAAC,OAAO;gBACzB,KAAK,EAAE,EAAE;aACV,CAAC;YACF,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,GAAG,CAAC,QAAQ,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACrE,GAAG,CAAC,QAAQ,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC;aAC/E,CAAC,CAAC;YACH,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YAC3C,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,CAAC,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5B,SAAS;QACT,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,MAAM,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QAC1C,SAAS,EAAE,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QAChD,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QAC5C,MAAM,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QAC1C,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;aAClC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;aACpC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC;YAC7B,GAAG,CAAC,KAAK,KAAK,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;YAC7C,UAAU,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC;SACvD,CAAC,CAAC;KACN,CAAC,CAAC;SACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAExD,MAAM,SAAS,GAAG,IAAI,GAAG,EAA4B,CAAC;IACtD,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS;YAAE,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvF,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,KAAK;QACf,UAAU;QACV,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC;QAC5B,WAAW,EAAE,aAAa,CAAC,UAAU,EAAE,SAAS,CAAC;KAClD,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,QAAQ,CAAC,UAAqC;IACrD,MAAM,MAAM,GAAW,EAAE,CAAC;IAE1B,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;QAC/B,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YAClC,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;gBACzC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;gBACpE,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC;oBAAE,SAAS;gBAE9B,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;gBACtE,MAAM,CAAC,IAAI,CAAC;oBACV,SAAS,EAAE,KAAK,CAAC,SAAS;oBAC1B,GAAG,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;oBAC5D,SAAS,EAAE,SAAS,CAAC,SAAS;oBAC9B,KAAK,EAAE,SAAS,CAAC,KAAK;oBACtB,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;iBAC9C,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAChB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,UAAU,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAClF,CAAC;AACJ,CAAC;AAGD;;;;;;;;;;;;;;GAcG;AACH,SAAS,oBAAoB,CAAC,QAAuC;IACnE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;YACzC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACpC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,KAAK,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,MAAM,EAAE,CAAC;QACvC,IAAI,IAAI,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM;YAAE,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAS,SAAS,CAChB,SAAuC,EACvC,UAA+B;IAE/B,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAE1F,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;QACnG,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QAClE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAClC,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,CAAC;IAED,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,0FAA0F;AAC1F,SAAS,YAAY,CAAC,IAAkC,EAAE,UAA8B;IACtF,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC;IACvE,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IACnE,OAAO,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,aAAa,GAAG,UAAU,CAAC;AAkBjC,SAAS,UAAU,CAAC,OAAiC,EAAE,SAAiB;IACtE,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACxC,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IAE5C,MAAM,OAAO,GAAgB;QAC3B,QAAQ,EAAE,EAAE;QACZ,SAAS,EAAE,CAAC;QACZ,QAAQ,EAAE,EAAE;QACZ,MAAM,EAAE,IAAI,GAAG,EAAE;QACjB,SAAS,EAAE,IAAI,GAAG,EAAE;QACpB,OAAO,EAAE,IAAI,GAAG,EAAE;QAClB,MAAM,EAAE,IAAI,GAAG,EAAE;QACjB,OAAO,EAAE,IAAI,GAAG,EAAE;KACnB,CAAC;IACF,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAChC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,WAAW,CAAC,CAAY,EAAE,CAAY;IAC7C,OAAO,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,UAAU,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;AACjF,CAAC;AAED;;;;;;GAMG;AACH,SAAS,UAAU,CAAC,CAAS,EAAE,CAAS;IACtC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC","sourcesContent":["import type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticSnapshot } from '../format/snapshot.js';\nimport { divergencesOf, type Divergence } from './divergence.js';\nimport { attributed, type ComponentInstance } from './instances.js';\n\n/**\n * The suite read as one graph, at one commit.\n *\n * Every other comparison in this system is between two revisions of one subject.\n * This is the other axis: **many subjects, one revision**, joined on the\n * components they share. It answers the question a per-subject tool cannot even\n * phrase — *this component, here, is the same component you already have an\n * example of over there* — and it answers it from digests a run already\n * computed, with no second render, no image and no store.\n *\n * ## What the join is worth\n *\n * A suite of examples is a suite of *compositions*. The narrow example is a\n * component at a boundary; the page examples contain that same boundary again,\n * sometimes with the same props and sometimes not. Once the instances are\n * addressable, three facts fall out that nobody had:\n *\n * - **An echo.** One rendering, several subjects. The narrow example and the\n * page are watching literally the same bytes, so a change in one is a change\n * in all of them, and a reviewer looking at eleven diffs is looking at one.\n * - **A divergence.** Same component, same props, *different* rendering, at one\n * commit. Nothing about the component's own inputs can explain that. Either\n * something outside it decides its output — an ancestor's cascade, a token, its\n * own state — or the reading is not repeatable. Both are findings and neither\n * is a regression.\n * - **A control group.** For any instance that moved, the same rendering\n * somewhere else that did *not* is a stable state to refer to. That is the\n * referent flake attribution has always needed and never had, and the suite\n * supplies it for free: one commit already holds N renderings of every shared\n * component.\n *\n * ## What it deliberately does not do\n *\n * It never decides anything. Two subjects sharing a rendering is not a reason to\n * delete either — that is the argument `coverage.ts` makes at the level of names\n * and it holds harder here, because a component can be correct in one context\n * and broken in the next, which is why the contexts are separate subjects.\n */\n\n/** One instance, located. */\nexport interface Site {\n readonly subject: string;\n readonly path: NodePath;\n readonly depth: number;\n readonly within?: string;\n\n /**\n * The component whose JSX wrote this element, here.\n *\n * On the entry beside it, `createdBy` is a set folded over the whole suite —\n * every component that ever wrote one of these, anywhere. That is the right\n * shape for *who mounts this* and the wrong one for *who mounted this here*,\n * and the ladder asks the second: an edit to a component that writes a `Chip`\n * on the footer explains nothing about a `Chip` the sidebar wrote. Kept per\n * site so the question can be asked where it was answered.\n */\n readonly createdBy?: string;\n}\n\n/** One rendering of one component, and everywhere it occurred. */\nexport interface Rendering {\n readonly rendering: Digest;\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n\n /**\n * Child components this rendering mounted, in document order.\n *\n * Carried on the rendering rather than only on the entry because it is the\n * only evidence available about what was passed *in*: `propsDigest` excludes\n * `children` by design, so two renderings that mount different children are\n * two different inputs wearing one props digest. `divergencesOf` refuses to\n * call that a contradiction, and this is the field it reads.\n */\n readonly renders: readonly string[];\n\n /** In subject order, then document order. */\n readonly sites: readonly Site[];\n}\n\n/** One props digest of one component, and every way it rendered under it. */\nexport interface PropsClass {\n /** Absent when the collector supplied no provenance — unknown, not \"none\". */\n readonly props?: Digest;\n readonly renderings: readonly Rendering[];\n}\n\nexport interface ComponentEntry {\n readonly component: string;\n /** Subjects containing at least one boundary of it, in the order supplied. */\n readonly subjects: readonly string[];\n /** Boundaries summed across every subject. Distinct from `subjects.length`. */\n readonly instances: number;\n\n /**\n * Subjects whose shallowest attributed boundary is this component.\n *\n * The narrow example — the subject that exists to show this thing, rather than\n * a page that happens to contain it. Defined by depth rather than by a naming\n * convention because a naming convention is a different tool's `story:` prefix,\n * and this has to hold for a route suite too. Ties resolve to nothing: a\n * subject whose two shallowest boundaries are siblings has no single subject.\n */\n readonly examples: readonly string[];\n\n /** Components that enclose it somewhere, sorted. The graph, upwards. */\n readonly within: readonly string[];\n\n /**\n * Components that *mounted* it somewhere, sorted. The graph, upwards, again —\n * and usually the more useful of the two.\n *\n * `within` is where the boundary sits in the DOM and this is who wrote the\n * element. They are the same thing only for a component that authors a node of\n * its own, and a real application is full of components that do not: measured\n * on `examples/todomvc`, every `Chip` reports `within: ['Stack']` and\n * `createdBy: ['TodoFooter']`: the `Stack` that encloses a chip is a layout\n * primitive that knows nothing about it, and the footer that wrote the element\n * stands one boundary further out. A component that renders nothing but other\n * components owns no node, is a boundary nowhere, and appears in this graph\n * *only* here — while being the file a reviewer has to open.\n *\n * Empty on a production build, where `_debugOwner` is absent (ADR-0007). Empty\n * is *not* \"nothing mounted it\": a component with no caller is the subject root\n * and every other case is missing data, which is why the `upstream` rung in\n * `movement.ts` reads this and never concludes from its silence.\n */\n readonly createdBy: readonly string[];\n\n /** Components it encloses somewhere, sorted. The graph, downwards. */\n readonly renders: readonly string[];\n\n /**\n * Custom properties its own nodes resolve through, anywhere in the suite.\n *\n * The set a token movement is intersected against, which is how \"`--va-space-3`\n * moved\" becomes \"`--va-space-3` moved and these four components read it\".\n */\n readonly tokens: readonly string[];\n\n /** Sorted by props digest; the class with unknown props sorts last. */\n readonly classes: readonly PropsClass[];\n}\n\n/**\n * The same component, the same props, the same bytes, in more than one subject.\n *\n * The connected dots. `example` names the narrow subject among the sites when\n * there is one, because that is the artefact a reviewer already has an opinion\n * about — \"this is the Chip from the Chip story\" is a sentence, and \"these four\n * paths share a digest\" is not.\n */\nexport interface Echo {\n readonly component: string;\n readonly props?: Digest;\n readonly rendering: Digest;\n /** At least two, spanning at least two subjects. */\n readonly sites: readonly Site[];\n readonly example?: string;\n}\n\n\nexport interface SubjectComposition {\n readonly subject: string;\n readonly instances: readonly ComponentInstance[];\n\n /**\n * The subject as read, kept so a divergence can say *which input* moved.\n *\n * Optional because `composeSubjects` is a fold over instance lists and stays\n * one — a caller holding only a report's sidecars still gets the graph, the\n * echoes and the divergences, and gets them without a `partings` field it\n * would have to explain away as empty.\n */\n readonly snapshot?: SemanticSnapshot;\n}\n\nexport interface Composition {\n /** In the order supplied, which the run guarantees is plan order. */\n readonly subjects: readonly string[];\n /** Sorted by name, code-unit order. */\n readonly components: readonly ComponentEntry[];\n /** Sorted by how many sites each has, widest first, then by component. */\n readonly echoes: readonly Echo[];\n /** Sorted by component name. */\n readonly divergences: readonly Divergence[];\n}\n\n/**\n * Fold a run's per-subject instance lists into the graph.\n *\n * Pure, ordered, and a function of its input alone — the run's report has to be\n * a function of the plan, and a phase that accumulated as a worker pool finished\n * would produce a different artefact from the same suite on a slower machine.\n */\nexport function composeSubjects(subjects: readonly SubjectComposition[]): Composition {\n const entries = new Map<string, Accumulator>();\n const order = subjects.map((subject) => subject.subject);\n const structural = structuralComponents(subjects);\n\n for (const subject of subjects) {\n const example = exampleOf(subject.instances, structural);\n\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n\n const entry = accumulate(entries, instance.component);\n if (entry.subjects.at(-1) !== subject.subject) entry.subjects.push(subject.subject);\n entry.instances += 1;\n if (example === instance.component && entry.examples.at(-1) !== subject.subject) {\n entry.examples.push(subject.subject);\n }\n if (instance.within !== undefined) entry.within.add(instance.within);\n if (instance.createdBy !== undefined) entry.createdBy.add(instance.createdBy);\n for (const child of instance.renders) entry.renders.add(child);\n for (const token of instance.tokens) entry.tokens.add(token);\n\n const key = instance.props ?? UNKNOWN_PROPS;\n const byProps = entry.classes.get(key) ?? new Map<Digest, MutableRendering>();\n const rendering = byProps.get(instance.rendering) ?? {\n rendering: instance.rendering,\n structure: instance.structure,\n semantics: instance.semantics,\n text: instance.text,\n style: instance.style,\n renders: instance.renders,\n sites: [],\n };\n rendering.sites.push({\n subject: subject.subject,\n path: instance.path,\n depth: instance.depth,\n ...(instance.within === undefined ? {} : { within: instance.within }),\n ...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),\n });\n byProps.set(instance.rendering, rendering);\n entry.classes.set(key, byProps);\n }\n }\n\n const components = [...entries.entries()]\n .map(([component, entry]) => ({\n component,\n subjects: entry.subjects,\n instances: entry.instances,\n examples: entry.examples,\n within: [...entry.within].sort(byCodeUnit),\n createdBy: [...entry.createdBy].sort(byCodeUnit),\n renders: [...entry.renders].sort(byCodeUnit),\n tokens: [...entry.tokens].sort(byCodeUnit),\n classes: [...entry.classes.entries()]\n .sort(([a], [b]) => byCodeUnit(a, b))\n .map(([props, renderings]) => ({\n ...(props === UNKNOWN_PROPS ? {} : { props }),\n renderings: [...renderings.values()].sort(widestFirst),\n })),\n }))\n .sort((a, b) => byCodeUnit(a.component, b.component));\n\n const snapshots = new Map<string, SemanticSnapshot>();\n for (const subject of subjects) {\n if (subject.snapshot !== undefined) snapshots.set(subject.subject, subject.snapshot);\n }\n\n return {\n subjects: order,\n components,\n echoes: echoesOf(components),\n divergences: divergencesOf(components, snapshots),\n };\n}\n\n/**\n * Every rendering shared by two or more subjects.\n *\n * Two instances of one component inside a *single* subject sharing a rendering\n * is ordinary and says nothing — a list of three identical chips is three\n * identical chips. The finding is a rendering that survives being mounted\n * somewhere else, because that is the one a second subject is watching.\n */\nfunction echoesOf(components: readonly ComponentEntry[]): readonly Echo[] {\n const echoes: Echo[] = [];\n\n for (const entry of components) {\n for (const group of entry.classes) {\n for (const rendering of group.renderings) {\n const spread = new Set(rendering.sites.map((site) => site.subject));\n if (spread.size < 2) continue;\n\n const example = entry.examples.find((subject) => spread.has(subject));\n echoes.push({\n component: entry.component,\n ...(group.props === undefined ? {} : { props: group.props }),\n rendering: rendering.rendering,\n sites: rendering.sites,\n ...(example === undefined ? {} : { example }),\n });\n }\n }\n }\n\n return echoes.sort(\n (a, b) => b.sites.length - a.sites.length || byCodeUnit(a.component, b.component),\n );\n}\n\n\n/**\n * Components more than half the suite mounts.\n *\n * Structure, not subject matter. A harness wrapper, a theme or store provider,\n * a portal root and an HOC that every screen is wrapped in are all mounted by\n * nearly every subject, and none of them is what any subject is about. The test\n * is a count over the suite rather than a list of names, which is the only form\n * that survives contact with a real application: nothing here knows that\n * `withStyles(x)` is an HOC, that a minified `aL` is a decorator, or that a\n * class component is a context consumer, and nothing here needs to.\n *\n * More than half, because that is where the claim becomes checkable. A\n * component two thirds of a suite mounts cannot be what distinguishes a subject\n * from its neighbours, whatever it is called.\n */\nfunction structuralComponents(subjects: readonly SubjectComposition[]): ReadonlySet<string> {\n const mounts = new Map<string, number>();\n for (const subject of subjects) {\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n mounts.set(instance.component, (mounts.get(instance.component) ?? 0) + 1);\n }\n }\n\n const structural = new Set<string>();\n for (const [component, held] of mounts) {\n if (held * 2 > subjects.length) structural.add(component);\n }\n return structural;\n}\n\n/**\n * The component a subject exists to show, when one component does.\n *\n * The shallowest attributed boundary that is not structure, and only when it is\n * alone at its depth. A story usually mounts one thing; a page mounts a layout\n * that mounts several, and calling the first of them the subject's component\n * would be picking a winner out of document order.\n *\n * The descent is the whole of the difference between this and the obvious rule.\n * Taking the shallowest boundary outright answers with whatever the harness\n * mounted first, which on a real suite is one value for every subject in it —\n * `Wrapper` across a component library, a minified decorator root across a\n * built Storybook — and a field with one value is a field that says nothing. A\n * depth every one of whose names is structure is passed through however many\n * names it has, because a harness that mounts a provider beside a portal root\n * has still not said what the subject is about.\n *\n * Where the descent finds nothing it answers as the shallowest rule would, so\n * this can name more subjects than that rule and never fewer.\n */\nfunction exampleOf(\n instances: readonly ComponentInstance[],\n structural: ReadonlySet<string>,\n): string | undefined {\n const held = instances.filter(attributed);\n const depths = [...new Set(held.map((instance) => instance.depth))].sort((a, b) => a - b);\n\n for (const depth of depths) {\n const names = new Set(held.filter((instance) => instance.depth === depth).map((i) => i.component));\n const matter = [...names].filter((name) => !structural.has(name));\n if (matter.length === 0) continue;\n return matter.length === 1 ? matter[0] : shallowestOf(held, depths[0]);\n }\n\n return shallowestOf(held, depths[0]);\n}\n\n/** The shallowest boundary when it is alone at its depth: the rule before the descent. */\nfunction shallowestOf(held: readonly ComponentInstance[], shallowest: number | undefined): string | undefined {\n if (shallowest === undefined) return undefined;\n const leads = held.filter((instance) => instance.depth === shallowest);\n const names = new Set(leads.map((instance) => instance.component));\n return names.size === 1 ? leads[0]?.component : undefined;\n}\n\n/**\n * The key an instance with no props digest is filed under.\n *\n * A sentinel rather than `undefined`, because a `Map` keyed on `Digest |\n * undefined` reads as though unknown props were a props class like any other,\n * and every consumer would then have to remember that it is not. It sorts last\n * by construction — no digest starts with a space.\n */\nconst UNKNOWN_PROPS = ' unknown';\n\ninterface Accumulator {\n readonly subjects: string[];\n instances: number;\n readonly examples: string[];\n readonly within: Set<string>;\n readonly createdBy: Set<string>;\n readonly renders: Set<string>;\n readonly tokens: Set<string>;\n readonly classes: Map<string, Map<Digest, MutableRendering>>;\n}\n\n/** A `Rendering` while its sites are still being collected. */\ninterface MutableRendering extends Omit<Rendering, 'sites'> {\n readonly sites: Site[];\n}\n\nfunction accumulate(entries: Map<string, Accumulator>, component: string): Accumulator {\n const existing = entries.get(component);\n if (existing !== undefined) return existing;\n\n const created: Accumulator = {\n subjects: [],\n instances: 0,\n examples: [],\n within: new Set(),\n createdBy: new Set(),\n renders: new Set(),\n tokens: new Set(),\n classes: new Map(),\n };\n entries.set(component, created);\n return created;\n}\n\nfunction widestFirst(a: Rendering, b: Rendering): number {\n return b.sites.length - a.sites.length || byCodeUnit(a.rendering, b.rendering);\n}\n\n/**\n * Code-unit order, never `localeCompare`.\n *\n * The same rule `hashComponents` sorts by, for the same reason: this output\n * reaches a report that is committed, diffed and read back on another runner,\n * and a locale-aware comparison makes the byte order a promise about `LANG`.\n */\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
@@ -72,6 +72,12 @@ export function divergencesOf(components, snapshots) {
72
72
  /**
73
73
  * Read every rendering after the first against the first, for the moved input.
74
74
  *
75
+ * `elsewhere`, because that is literally what these two are: one instance here
76
+ * and one instance there, at one commit. It is the difference between this
77
+ * finding saying a component is nondeterministic and saying its context decided
78
+ * something — and only the second is true of a thing that moved because the box
79
+ * around it is a different size.
80
+ *
75
81
  * Each site is lifted out of its subject and re-rooted at the component before
76
82
  * the comparison — {@link boundarySnapshot}'s reason for existing. Comparing the
77
83
  * two *subjects* instead would compare a receipt against a promo card, which is
@@ -92,7 +98,7 @@ function partingsOf(renderings, snapshots) {
92
98
  const other = liftFirstSite(rendering, snapshots);
93
99
  if (other === undefined)
94
100
  continue;
95
- found.push({ rendering: index, lines: explainParting(partingOf(first, other)) });
101
+ found.push({ rendering: index, lines: explainParting(partingOf(first, other, 'elsewhere')) });
96
102
  }
97
103
  return found;
98
104
  }
@@ -1 +1 @@
1
- {"version":3,"file":"divergence.js","sourceRoot":"","sources":["../../src/attribute/divergence.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Band } from '../compare/band.js';\nimport { explainParting } from '../compare/explain.js';\nimport { boundarySnapshot } from '../compare/instance.js';\nimport { partingOf } from '../compare/parting.js';\nimport type { Digest } from '../format/hash.js';\nimport type { SemanticSnapshot } from '../format/snapshot.js';\nimport { movedBands } from './component-hash.js';\nimport type { ComponentEntry, Rendering } from './composition.js';\n\n/**\n * The finding with no baseline in it, and the sentence that makes it actionable.\n *\n * Split out of `composition.ts` because it is the only part of the graph that\n * reads *documents*. Everything else there folds digests a run already computed;\n * this lifts two instances out of the pages they were found in and compares them\n * — which is the only way to say `color`, and the difference between a reader\n * knowing which subject to open and knowing what is waiting there.\n */\n\n/**\n * The same component, the same inputs, and more than one rendering. At one commit.\n *\n * Not a regression and not a comparison — there is no baseline anywhere in it.\n * It is a statement that the component's own inputs do not determine its output,\n * which is either a fact about the design (a token, a theme, an ancestor) or the\n * reading not being repeatable. `bands` says which kind of difference it is, in\n * the same vocabulary a sensitivity absorbs, so a divergence entirely inside a\n * band the subject relaxes is one a reader can dismiss without opening it.\n *\n * *Inputs*, not *props*, and the difference is the whole finding: a props digest\n * excludes `children` and the checks in `divergencesOf` are what close the gap.\n * A suite can legitimately produce none of these — `examples/todomvc` produces\n * exactly zero — and that is the correct answer for a suite in which nothing\n * renders two ways from one input, not a section to be filled.\n */\nexport interface Divergence {\n readonly component: string;\n readonly props?: Digest;\n readonly bands: readonly Band[];\n /** At least two, sorted by how many sites each has, widest first. */\n readonly renderings: readonly Rendering[];\n\n /**\n * Why each rendering after the first parted from it.\n *\n * The half of this finding that used to be missing. \"`Price` rendered two ways\n * from one props digest\" states the contradiction and stops there, leaving the\n * reader to open two subjects and diff them by eye — which is the work the\n * component graph was built to remove. These lines name the input: an\n * ancestor's `color`, a context, a hook cell, or nothing readable at all.\n *\n * It is reachable here and nowhere else in the system, because a parting needs\n * both sides read the same way and this is the only comparison where that is\n * true by construction: both renderings come out of one run, off one collector,\n * at one commit. A parting across two revisions has to reconcile two configs\n * and two baselines first.\n *\n * Absent when the run supplied no snapshots to read — unknown, not \"nothing to\n * say\" (ADR-0002). Shorter than `renderings.length - 1` when some rendering's\n * site could not be lifted.\n */\n readonly partings?: readonly DivergenceParting[];\n}\n\n/** One rendering of a divergence, and why it parted from the first. */\nexport interface DivergenceParting {\n /** Index into {@link Divergence.renderings}, always at least 1. */\n readonly rendering: number;\n /** `explainParting` output, comparing that rendering against the first. */\n readonly lines: readonly string[];\n}\n\n/**\n * Every props class that produced more than one rendering *from one input*.\n *\n * Three refusals, and all three are the same refusal: a props digest is not a\n * complete statement of a component's inputs, so most pairs of renderings that\n * share one are not a contradiction. Measured on `examples/todomvc` before the\n * checks below existed, **eleven divergences were reported and all eleven were\n * false** — which is what a finding built on an incomplete key looks like.\n *\n * **Unknown props are not shared props.** Instances whose provenance did not\n * survive are not known to have received the same thing. Grouping them and\n * reporting that they render differently manufactures a finding out of missing\n * data, in a system where absent must never read as equal.\n *\n * **Renderings that co-occur in one subject are not alternatives.** A component\n * whose nodes are interrupted by a nested boundary is walked as two boundaries\n * with one owner frame, so one `TextField` becomes a label-shaped rendering and\n * an input-shaped one under a single props digest. That is one instance in two\n * pieces, and it is indistinguishable from two instances that genuinely disagree\n * — so it is not reported. A contradiction is a component that renders as A\n * *here* and as B *there*, never both at once.\n *\n * **Different children are different inputs.** `propsDigest` excludes `children`\n * deliberately (see `digestableProps` in `@variance-authority/react`: folding the\n * subtree in would make every ancestor's props move on any descendant edit, and\n * §6.2's root/collateral rule could never fire). The consequence is that\n * `<Card><Stack/></Card>` and `<Card><Text/></Card>` share a props digest, and\n * calling their different output a contradiction blames the component for its\n * caller. Two proxies for \"the children differed\" are available and both are\n * required to be quiet: the child components mounted, and the boundary's own\n * text — which is where a string child lands.\n *\n * What survives is narrow on purpose. A movement wrongly dismissed as\n * `contradicted` is an explanation nobody can act on; the same movement left\n * unexplained lands on the suspect shortlist, where a second reading settles it.\n * The asymmetry is the whole reason these checks are here rather than in prose.\n */\nexport function divergencesOf(\n components: readonly ComponentEntry[],\n snapshots: ReadonlyMap<string, SemanticSnapshot>,\n): readonly Divergence[] {\n const divergences: Divergence[] = [];\n\n for (const entry of components) {\n for (const group of entry.classes) {\n if (group.props === undefined || group.renderings.length < 2) continue;\n if (!fromOneInput(group.renderings)) continue;\n\n // Union against the first rather than over every pair. A band moves here\n // when the renderings do not all agree on its digest, and a field that\n // disagrees anywhere disagrees with the first somewhere — so the two are\n // the same set, and the mapping stays in `movedBands` where the sensitivity\n // tier reads it.\n const [first, ...rest] = group.renderings;\n const bands = new Set<Band>();\n for (const other of rest) for (const band of movedBands(first!, other)) bands.add(band);\n\n const partings = partingsOf(group.renderings, snapshots);\n\n divergences.push({\n component: entry.component,\n props: group.props,\n bands: [...bands],\n renderings: group.renderings,\n ...(partings === undefined ? {} : { partings }),\n });\n }\n }\n\n return divergences.sort((a, b) => byCodeUnit(a.component, b.component));\n}\n\n/**\n * Read every rendering after the first against the first, for the moved input.\n *\n * Each site is lifted out of its subject and re-rooted at the component before\n * the comparison — {@link boundarySnapshot}'s reason for existing. Comparing the\n * two *subjects* instead would compare a receipt against a promo card, which is\n * the difference the reader already knows about and not the one being asked.\n *\n * `undefined`, never `[]`, when no snapshot for the first rendering's subject was\n * supplied: a run that was never handed the documents has not found the parting\n * unexplainable, it has not looked.\n */\nfunction partingsOf(\n renderings: readonly Rendering[],\n snapshots: ReadonlyMap<string, SemanticSnapshot>,\n): readonly DivergenceParting[] | undefined {\n const first = liftFirstSite(renderings[0], snapshots);\n if (first === undefined) return undefined;\n\n const found: DivergenceParting[] = [];\n for (const [index, rendering] of renderings.entries()) {\n if (index === 0) continue;\n const other = liftFirstSite(rendering, snapshots);\n if (other === undefined) continue;\n found.push({ rendering: index, lines: explainParting(partingOf(first, other)) });\n }\n return found;\n}\n\n/**\n * One rendering's first site, as a snapshot rooted at the component.\n *\n * The first site and not a chosen one: `sites` is ordered by subject then\n * document position, so this is the same instance on every machine — which the\n * report being a function of the plan requires.\n */\nfunction liftFirstSite(\n rendering: Rendering | undefined,\n snapshots: ReadonlyMap<string, SemanticSnapshot>,\n): SemanticSnapshot | undefined {\n const site = rendering?.sites[0];\n if (site === undefined) return undefined;\n const snapshot = snapshots.get(site.subject);\n if (snapshot === undefined) return undefined;\n return boundarySnapshot(snapshot, site.path);\n}\n\n/**\n * Whether a props class' renderings can be said to have had the same inputs.\n *\n * The two checks the props digest cannot make for itself — see `divergencesOf`\n * above for why each exists. Both are conservative in the same direction: they\n * answer *no* whenever the run cannot tell, so what remains is a set of\n * renderings that mounted the same children, said the same words, and still came\n * out different, with no two of them observed in one subject.\n */\nfunction fromOneInput(renderings: readonly Rendering[]): boolean {\n const [first, ...rest] = renderings;\n if (first === undefined) return false;\n\n if (rest.some((other) => other.text !== first.text)) return false;\n if (rest.some((other) => !sameOrder(other.renders, first.renders))) return false;\n\n const seen = new Set<string>();\n for (const rendering of renderings) {\n for (const subject of new Set(rendering.sites.map((site) => site.subject))) {\n if (seen.has(subject)) return false;\n seen.add(subject);\n }\n }\n\n return true;\n}\n\nfunction sameOrder(a: readonly string[], b: readonly string[]): boolean {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n}\n\n/**\n * Code-unit order, never `localeCompare`, for `composition.ts`' reason: this\n * output reaches a report that is committed and read back on another runner.\n */\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
1
+ {"version":3,"file":"divergence.js","sourceRoot":"","sources":["../../src/attribute/divergence.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Band } from '../compare/band.js';\nimport { explainParting } from '../compare/explain.js';\nimport { boundarySnapshot } from '../compare/instance.js';\nimport { partingOf } from '../compare/parting.js';\nimport type { Digest } from '../format/hash.js';\nimport type { SemanticSnapshot } from '../format/snapshot.js';\nimport { movedBands } from './component-hash.js';\nimport type { ComponentEntry, Rendering } from './composition.js';\n\n/**\n * The finding with no baseline in it, and the sentence that makes it actionable.\n *\n * Split out of `composition.ts` because it is the only part of the graph that\n * reads *documents*. Everything else there folds digests a run already computed;\n * this lifts two instances out of the pages they were found in and compares them\n * — which is the only way to say `color`, and the difference between a reader\n * knowing which subject to open and knowing what is waiting there.\n */\n\n/**\n * The same component, the same inputs, and more than one rendering. At one commit.\n *\n * Not a regression and not a comparison — there is no baseline anywhere in it.\n * It is a statement that the component's own inputs do not determine its output,\n * which is either a fact about the design (a token, a theme, an ancestor) or the\n * reading not being repeatable. `bands` says which kind of difference it is, in\n * the same vocabulary a sensitivity absorbs, so a divergence entirely inside a\n * band the subject relaxes is one a reader can dismiss without opening it.\n *\n * *Inputs*, not *props*, and the difference is the whole finding: a props digest\n * excludes `children` and the checks in `divergencesOf` are what close the gap.\n * A suite can legitimately produce none of these — `examples/todomvc` produces\n * exactly zero — and that is the correct answer for a suite in which nothing\n * renders two ways from one input, not a section to be filled.\n */\nexport interface Divergence {\n readonly component: string;\n readonly props?: Digest;\n readonly bands: readonly Band[];\n /** At least two, sorted by how many sites each has, widest first. */\n readonly renderings: readonly Rendering[];\n\n /**\n * Why each rendering after the first parted from it.\n *\n * The half of this finding that used to be missing. \"`Price` rendered two ways\n * from one props digest\" states the contradiction and stops there, leaving the\n * reader to open two subjects and diff them by eye — which is the work the\n * component graph was built to remove. These lines name the input: an\n * ancestor's `color`, a context, a hook cell, or nothing readable at all.\n *\n * It is reachable here and nowhere else in the system, because a parting needs\n * both sides read the same way and this is the only comparison where that is\n * true by construction: both renderings come out of one run, off one collector,\n * at one commit. A parting across two revisions has to reconcile two configs\n * and two baselines first.\n *\n * Absent when the run supplied no snapshots to read — unknown, not \"nothing to\n * say\" (ADR-0002). Shorter than `renderings.length - 1` when some rendering's\n * site could not be lifted.\n */\n readonly partings?: readonly DivergenceParting[];\n}\n\n/** One rendering of a divergence, and why it parted from the first. */\nexport interface DivergenceParting {\n /** Index into {@link Divergence.renderings}, always at least 1. */\n readonly rendering: number;\n /** `explainParting` output, comparing that rendering against the first. */\n readonly lines: readonly string[];\n}\n\n/**\n * Every props class that produced more than one rendering *from one input*.\n *\n * Three refusals, and all three are the same refusal: a props digest is not a\n * complete statement of a component's inputs, so most pairs of renderings that\n * share one are not a contradiction. Measured on `examples/todomvc` before the\n * checks below existed, **eleven divergences were reported and all eleven were\n * false** — which is what a finding built on an incomplete key looks like.\n *\n * **Unknown props are not shared props.** Instances whose provenance did not\n * survive are not known to have received the same thing. Grouping them and\n * reporting that they render differently manufactures a finding out of missing\n * data, in a system where absent must never read as equal.\n *\n * **Renderings that co-occur in one subject are not alternatives.** A component\n * whose nodes are interrupted by a nested boundary is walked as two boundaries\n * with one owner frame, so one `TextField` becomes a label-shaped rendering and\n * an input-shaped one under a single props digest. That is one instance in two\n * pieces, and it is indistinguishable from two instances that genuinely disagree\n * — so it is not reported. A contradiction is a component that renders as A\n * *here* and as B *there*, never both at once.\n *\n * **Different children are different inputs.** `propsDigest` excludes `children`\n * deliberately (see `digestableProps` in `@variance-authority/react`: folding the\n * subtree in would make every ancestor's props move on any descendant edit, and\n * §6.2's root/collateral rule could never fire). The consequence is that\n * `<Card><Stack/></Card>` and `<Card><Text/></Card>` share a props digest, and\n * calling their different output a contradiction blames the component for its\n * caller. Two proxies for \"the children differed\" are available and both are\n * required to be quiet: the child components mounted, and the boundary's own\n * text — which is where a string child lands.\n *\n * What survives is narrow on purpose. A movement wrongly dismissed as\n * `contradicted` is an explanation nobody can act on; the same movement left\n * unexplained lands on the suspect shortlist, where a second reading settles it.\n * The asymmetry is the whole reason these checks are here rather than in prose.\n */\nexport function divergencesOf(\n components: readonly ComponentEntry[],\n snapshots: ReadonlyMap<string, SemanticSnapshot>,\n): readonly Divergence[] {\n const divergences: Divergence[] = [];\n\n for (const entry of components) {\n for (const group of entry.classes) {\n if (group.props === undefined || group.renderings.length < 2) continue;\n if (!fromOneInput(group.renderings)) continue;\n\n // Union against the first rather than over every pair. A band moves here\n // when the renderings do not all agree on its digest, and a field that\n // disagrees anywhere disagrees with the first somewhere — so the two are\n // the same set, and the mapping stays in `movedBands` where the sensitivity\n // tier reads it.\n const [first, ...rest] = group.renderings;\n const bands = new Set<Band>();\n for (const other of rest) for (const band of movedBands(first!, other)) bands.add(band);\n\n const partings = partingsOf(group.renderings, snapshots);\n\n divergences.push({\n component: entry.component,\n props: group.props,\n bands: [...bands],\n renderings: group.renderings,\n ...(partings === undefined ? {} : { partings }),\n });\n }\n }\n\n return divergences.sort((a, b) => byCodeUnit(a.component, b.component));\n}\n\n/**\n * Read every rendering after the first against the first, for the moved input.\n *\n * `elsewhere`, because that is literally what these two are: one instance here\n * and one instance there, at one commit. It is the difference between this\n * finding saying a component is nondeterministic and saying its context decided\n * something — and only the second is true of a thing that moved because the box\n * around it is a different size.\n *\n * Each site is lifted out of its subject and re-rooted at the component before\n * the comparison — {@link boundarySnapshot}'s reason for existing. Comparing the\n * two *subjects* instead would compare a receipt against a promo card, which is\n * the difference the reader already knows about and not the one being asked.\n *\n * `undefined`, never `[]`, when no snapshot for the first rendering's subject was\n * supplied: a run that was never handed the documents has not found the parting\n * unexplainable, it has not looked.\n */\nfunction partingsOf(\n renderings: readonly Rendering[],\n snapshots: ReadonlyMap<string, SemanticSnapshot>,\n): readonly DivergenceParting[] | undefined {\n const first = liftFirstSite(renderings[0], snapshots);\n if (first === undefined) return undefined;\n\n const found: DivergenceParting[] = [];\n for (const [index, rendering] of renderings.entries()) {\n if (index === 0) continue;\n const other = liftFirstSite(rendering, snapshots);\n if (other === undefined) continue;\n found.push({ rendering: index, lines: explainParting(partingOf(first, other, 'elsewhere')) });\n }\n return found;\n}\n\n/**\n * One rendering's first site, as a snapshot rooted at the component.\n *\n * The first site and not a chosen one: `sites` is ordered by subject then\n * document position, so this is the same instance on every machine — which the\n * report being a function of the plan requires.\n */\nfunction liftFirstSite(\n rendering: Rendering | undefined,\n snapshots: ReadonlyMap<string, SemanticSnapshot>,\n): SemanticSnapshot | undefined {\n const site = rendering?.sites[0];\n if (site === undefined) return undefined;\n const snapshot = snapshots.get(site.subject);\n if (snapshot === undefined) return undefined;\n return boundarySnapshot(snapshot, site.path);\n}\n\n/**\n * Whether a props class' renderings can be said to have had the same inputs.\n *\n * The two checks the props digest cannot make for itself — see `divergencesOf`\n * above for why each exists. Both are conservative in the same direction: they\n * answer *no* whenever the run cannot tell, so what remains is a set of\n * renderings that mounted the same children, said the same words, and still came\n * out different, with no two of them observed in one subject.\n */\nfunction fromOneInput(renderings: readonly Rendering[]): boolean {\n const [first, ...rest] = renderings;\n if (first === undefined) return false;\n\n if (rest.some((other) => other.text !== first.text)) return false;\n if (rest.some((other) => !sameOrder(other.renders, first.renders))) return false;\n\n const seen = new Set<string>();\n for (const rendering of renderings) {\n for (const subject of new Set(rendering.sites.map((site) => site.subject))) {\n if (seen.has(subject)) return false;\n seen.add(subject);\n }\n }\n\n return true;\n}\n\nfunction sameOrder(a: readonly string[], b: readonly string[]): boolean {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n}\n\n/**\n * Code-unit order, never `localeCompare`, for `composition.ts`' reason: this\n * output reaches a report that is committed and read back on another runner.\n */\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
@@ -19,7 +19,7 @@ export { isolateRegions, subtractRegions, excludedBoxes } from './mask.js';
19
19
  export type { ChangeMask, DiffRegion, ExcludedBox, Isolation, IsolationOptions, Subtraction, } from './mask.js';
20
20
  export { attributeRegions, rankRegions } from './region.js';
21
21
  export type { AttributedRegion, AttributionOptions, RankedRegion } from './region.js';
22
- export { resolveSource, formatSource, indexSource, mergeSourceIndexes } from './source.js';
22
+ export { resolveSource, formatSource, indexSource, mergeSourceIndexes, overlaySourceIndex, } from './source.js';
23
23
  export type { SourceIndex, SourceRef, Resolution } from './source.js';
24
24
  export { parseStackFrames, isVendorPath, writerLocationOf } from './stack.js';
25
25
  export type { StackFrame } from './stack.js';
@@ -31,9 +31,15 @@ export { hashComponents, causesBetween, movedBands, movedBandsBetween, bandsBetw
31
31
  export type { BandDigests, ComponentBands, ComponentHash } from './component-hash.js';
32
32
  export { componentInstances, attributed } from './instances.js';
33
33
  export type { ComponentInstance } from './instances.js';
34
+ export { nodeClosures, sharedClosures } from './closure.js';
35
+ export type { ClosureSite, NodeClosure, SharedClosure, SharedClosureOptions, SubjectTree } from './closure.js';
34
36
  export { composeSubjects } from './composition.js';
35
37
  export type { Composition, ComponentEntry, Echo, PropsClass, Rendering, Site, SubjectComposition, } from './composition.js';
36
38
  export type { Divergence, DivergenceParting } from './divergence.js';
39
+ export { lexiconOf, structureOf, LEXICON_CAP } from './lexicon.js';
40
+ export { LANDMARK_CAP } from './landmark.js';
41
+ export type { Landmark } from './landmark.js';
42
+ export type { BoundaryRow, LexiconField, LexiconOptions, SubjectLexicon } from './lexicon.js';
37
43
  export { attributeMovement } from './movement.js';
38
44
  export type { Attribution, Cause, Evidence, Moved, Movement } from './movement.js';
39
45
  export { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';
@@ -16,13 +16,16 @@
16
16
  export { locate } from './locate.js';
17
17
  export { isolateRegions, subtractRegions, excludedBoxes } from './mask.js';
18
18
  export { attributeRegions, rankRegions } from './region.js';
19
- export { resolveSource, formatSource, indexSource, mergeSourceIndexes } from './source.js';
19
+ export { resolveSource, formatSource, indexSource, mergeSourceIndexes, overlaySourceIndex, } from './source.js';
20
20
  export { parseStackFrames, isVendorPath, writerLocationOf } from './stack.js';
21
21
  export { parseSourceMap, originalPositionFor, sourceMappingUrlOf, inlineSourceMapOf, } from './source-map.js';
22
22
  export { createCallSiteResolver, locateProvenance, locateSites } from './call-site.js';
23
23
  export { hashComponents, causesBetween, movedBands, movedBandsBetween, bandsBetween, UNATTRIBUTED, } from './component-hash.js';
24
24
  export { componentInstances, attributed } from './instances.js';
25
+ export { nodeClosures, sharedClosures } from './closure.js';
25
26
  export { composeSubjects } from './composition.js';
27
+ export { lexiconOf, structureOf, LEXICON_CAP } from './lexicon.js';
28
+ export { LANDMARK_CAP } from './landmark.js';
26
29
  export { attributeMovement } from './movement.js';
27
30
  export { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';
28
31
  export { locateInstability, summarizeInstability } from './instability.js';
@@ -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,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAG3F,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,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAanD,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 { resolveSource, formatSource, indexSource, mergeSourceIndexes } 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 } from './component-hash.js';\n\nexport { componentInstances, attributed } from './instances.js';\nexport type { ComponentInstance } from './instances.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 { 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,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,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACnE,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 } 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, structureOf, LEXICON_CAP } from './lexicon.js';\nexport { LANDMARK_CAP } from './landmark.js';\nexport type { Landmark } from './landmark.js';\nexport type { BoundaryRow, LexiconField, LexiconOptions, 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"]}
@@ -0,0 +1,97 @@
1
+ import type { SemanticNode } from '../format/snapshot.js';
2
+ /**
3
+ * Where each thing on a subject was, kept as the run read it.
4
+ *
5
+ * The bags next door say what a subject holds; these say where it held it, off
6
+ * the same walk over the same tree, because two walks eventually disagree about
7
+ * what was there. A node earns a place by bearing a role, an accessible name,
8
+ * or words of its own — never by what it is called — so a screen written under
9
+ * an era's worth of higher-order components reduces to the same landmarks as
10
+ * the same screen written flat.
11
+ */
12
+ /** One thing on a subject a person could point at, where the run saw it. */
13
+ export interface Landmark {
14
+ /** Accessible role, when the reading resolved one. */
15
+ readonly role?: string;
16
+ /** Accessible name — what a screen reader would announce. */
17
+ readonly name?: string;
18
+ /**
19
+ * The words directly inside it, and only those. Text belonging to a nested
20
+ * landmark stays on that landmark, so a container does not inherit the page.
21
+ */
22
+ readonly text?: string;
23
+ /** Index of the nearest enclosing landmark. Absent on a top-level one. */
24
+ readonly within?: number;
25
+ /**
26
+ * `[x, y, width, height]` in layout pixels, integers.
27
+ *
28
+ * Absent, never zeroed, exactly as `rect` is: a reading that did not resolve
29
+ * layout has no opinion about what sits beneath what, and must say so rather
30
+ * than answer from document order while sounding equally sure.
31
+ */
32
+ readonly box?: readonly [number, number, number, number];
33
+ /** Where the element was written, when the project installed the plugin. */
34
+ readonly file?: string;
35
+ readonly line?: number;
36
+ /**
37
+ * The innermost component that owns it, off the fiber's owner chain.
38
+ *
39
+ * The line an element was written at needs the JSX-source plugin, which a
40
+ * production build strips — and a built Storybook is one, so `file` and
41
+ * `line` are absent on exactly the runs that matter most. The owner chain
42
+ * survives that build: not the line, but the component whose source you would
43
+ * open, joined to a path through `LexiconReport.declaredIn`.
44
+ */
45
+ readonly component?: string;
46
+ /** The component whose JSX created it. The unit somebody owns. */
47
+ readonly createdBy?: string;
48
+ /** A test handle, when one was set. The one name a suite chose deliberately. */
49
+ readonly handle?: string;
50
+ }
51
+ /**
52
+ * Landmarks kept per subject.
53
+ *
54
+ * Higher than the cap above on purpose. A field that loses its two hundredth
55
+ * distinct value loses a word the subject also says somewhere else; a subject
56
+ * that loses its last landmark loses a *place*, and a place has no second
57
+ * spelling.
58
+ *
59
+ * Four hundred was a guess and a 572-subject application falsified it: p50 9,
60
+ * p90 52, p99 227, and four subjects pinned at the cap, losing 1,973 places
61
+ * between them off screens of roughly 1,179, 926, 836 and 632. Those four are
62
+ * its biggest screens, which are the ones somebody most needs orienting on, and
63
+ * what a cap cuts there is the bottom of the page. Twelve hundred clears the
64
+ * largest with room and leaves the other 539 an order of magnitude below it.
65
+ */
66
+ export declare const LANDMARK_CAP = 1200;
67
+ /** A digest is a coordinate, not a word: matching one matches a hash. */
68
+ export declare function isDigest(value: string): boolean;
69
+ /**
70
+ * Document order, carrying what encloses what.
71
+ *
72
+ * `visit` returns the index a child should call its enclosure, which is how
73
+ * containment survives a tree whose intermediate nodes are nearly all
74
+ * scaffolding: a landmark's `within` names the nearest *landmark* above it, not
75
+ * the nearest node, so the depth of the wrapper stack between them costs
76
+ * nothing.
77
+ */
78
+ export declare function walk(node: SemanticNode, within: number | undefined, visit: (node: SemanticNode, within: number | undefined) => number | undefined): void;
79
+ /**
80
+ * The landmark this node earns, or nothing.
81
+ *
82
+ * The test is what the node says. A text node's words belong to the element
83
+ * holding them, which was visited first and has already taken them — visited
84
+ * all the same, because the `text` bag is indexed from exactly these.
85
+ */
86
+ export declare function landmarkFor(node: SemanticNode, within: number | undefined): Landmark | undefined;
87
+ /**
88
+ * Which landmarks survive a subject that overran the cap.
89
+ *
90
+ * Words first: a landmark with a name or a sentence is one a question can
91
+ * reach, and a bare `generic` with a rectangle is one nothing will ever ask
92
+ * for. Ties keep document order, so what survives is still an arrangement.
93
+ * `within` is rewritten to the nearest surviving enclosure, because an index
94
+ * into a list that has been cut is otherwise a pointer at the wrong thing.
95
+ */
96
+ export declare function capLandmarks(landmarks: readonly Landmark[]): readonly Landmark[];
97
+ //# sourceMappingURL=landmark.d.ts.map
@@ -0,0 +1,144 @@
1
+ import { UNATTRIBUTED } from './boundary.js';
2
+ /**
3
+ * Landmarks kept per subject.
4
+ *
5
+ * Higher than the cap above on purpose. A field that loses its two hundredth
6
+ * distinct value loses a word the subject also says somewhere else; a subject
7
+ * that loses its last landmark loses a *place*, and a place has no second
8
+ * spelling.
9
+ *
10
+ * Four hundred was a guess and a 572-subject application falsified it: p50 9,
11
+ * p90 52, p99 227, and four subjects pinned at the cap, losing 1,973 places
12
+ * between them off screens of roughly 1,179, 926, 836 and 632. Those four are
13
+ * its biggest screens, which are the ones somebody most needs orienting on, and
14
+ * what a cap cuts there is the bottom of the page. Twelve hundred clears the
15
+ * largest with room and leaves the other 539 an order of magnitude below it.
16
+ */
17
+ export const LANDMARK_CAP = 1_200;
18
+ /** A digest is a coordinate, not a word: matching one matches a hash. */
19
+ export function isDigest(value) {
20
+ return /^v\d+:[0-9a-f]+$/.test(value);
21
+ }
22
+ /**
23
+ * Document order, carrying what encloses what.
24
+ *
25
+ * `visit` returns the index a child should call its enclosure, which is how
26
+ * containment survives a tree whose intermediate nodes are nearly all
27
+ * scaffolding: a landmark's `within` names the nearest *landmark* above it, not
28
+ * the nearest node, so the depth of the wrapper stack between them costs
29
+ * nothing.
30
+ */
31
+ export function walk(node, within, visit) {
32
+ const at = visit(node, within);
33
+ for (const child of node.children)
34
+ walk(child, at, visit);
35
+ }
36
+ /**
37
+ * The landmark this node earns, or nothing.
38
+ *
39
+ * The test is what the node says. A text node's words belong to the element
40
+ * holding them, which was visited first and has already taken them — visited
41
+ * all the same, because the `text` bag is indexed from exactly these.
42
+ */
43
+ export function landmarkFor(node, within) {
44
+ if (node.tag === '#text')
45
+ return undefined;
46
+ const text = ownTextOf(node);
47
+ const name = node.name ?? node.description ?? labelOf(node);
48
+ if (node.role === undefined && name === undefined && text === undefined)
49
+ return undefined;
50
+ return landmarkOf(node, text, name, within);
51
+ }
52
+ function landmarkOf(node, text, name, within) {
53
+ const source = node.provenance?.source;
54
+ const handle = node.attributes['data-testid'] ?? node.attributes['data-test-id'];
55
+ // `owners[0]` is the innermost frame: the component that rendered this node,
56
+ // not the one it was placed by. That is the file somebody opens.
57
+ const component = node.provenance?.owners?.[0]?.name;
58
+ return {
59
+ ...(node.role === undefined ? {} : { role: node.role }),
60
+ ...(name === undefined ? {} : { name }),
61
+ ...(text === undefined ? {} : { text }),
62
+ ...(within === undefined ? {} : { within }),
63
+ ...(node.rect === undefined
64
+ ? {}
65
+ : {
66
+ box: [
67
+ Math.round(node.rect.x),
68
+ Math.round(node.rect.y),
69
+ Math.round(node.rect.width),
70
+ Math.round(node.rect.height),
71
+ ],
72
+ }),
73
+ ...(source === undefined ? {} : { file: source.file, line: source.line }),
74
+ ...(component === undefined || component === UNATTRIBUTED ? {} : { component }),
75
+ ...(node.provenance?.createdBy === undefined ? {} : { createdBy: node.provenance.createdBy }),
76
+ ...(handle === undefined ? {} : { handle }),
77
+ };
78
+ }
79
+ /**
80
+ * The words this node says itself.
81
+ *
82
+ * Direct text children only. A container does not inherit the sentence its
83
+ * button holds, because the question a landmark answers is *where does this
84
+ * sentence live*, and an answer that says "on the page" is not one.
85
+ */
86
+ function ownTextOf(node) {
87
+ let held = '';
88
+ for (const child of node.children) {
89
+ if (child.tag !== '#text')
90
+ continue;
91
+ const text = child.text;
92
+ if (text === undefined || isDigest(text))
93
+ continue;
94
+ held = held === '' ? text : `${held} ${text}`;
95
+ }
96
+ const trimmed = held.replace(/\s+/g, ' ').trim();
97
+ return trimmed === '' ? undefined : trimmed;
98
+ }
99
+ /**
100
+ * A name for a node that resolved none, from the attributes that are a label
101
+ * for a person rather than a value for a machine. The same three `names` reads.
102
+ */
103
+ function labelOf(node) {
104
+ const held = node.attributes['placeholder'] ?? node.attributes['alt'] ?? node.attributes['title'];
105
+ if (held === undefined)
106
+ return undefined;
107
+ const trimmed = held.trim();
108
+ return trimmed === '' ? undefined : trimmed;
109
+ }
110
+ /**
111
+ * Which landmarks survive a subject that overran the cap.
112
+ *
113
+ * Words first: a landmark with a name or a sentence is one a question can
114
+ * reach, and a bare `generic` with a rectangle is one nothing will ever ask
115
+ * for. Ties keep document order, so what survives is still an arrangement.
116
+ * `within` is rewritten to the nearest surviving enclosure, because an index
117
+ * into a list that has been cut is otherwise a pointer at the wrong thing.
118
+ */
119
+ export function capLandmarks(landmarks) {
120
+ const worth = (landmark) => (landmark.name === undefined ? 0 : 2) + (landmark.text === undefined ? 0 : 1);
121
+ const keep = landmarks
122
+ .map((landmark, at) => ({ landmark, at }))
123
+ .sort((left, right) => worth(right.landmark) - worth(left.landmark) || left.at - right.at)
124
+ .slice(0, LANDMARK_CAP)
125
+ .sort((left, right) => left.at - right.at);
126
+ const moved = new Map();
127
+ keep.forEach(({ at }, to) => moved.set(at, to));
128
+ const enclosure = (at) => {
129
+ let held = at;
130
+ while (held !== undefined) {
131
+ const to = moved.get(held);
132
+ if (to !== undefined)
133
+ return to;
134
+ held = landmarks[held]?.within;
135
+ }
136
+ return undefined;
137
+ };
138
+ return keep.map(({ landmark }) => {
139
+ const within = enclosure(landmark.within);
140
+ const { within: _dropped, ...rest } = landmark;
141
+ return within === undefined ? rest : { ...rest, within };
142
+ });
143
+ }
144
+ //# sourceMappingURL=landmark.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"landmark.js","sourceRoot":"","sources":["../../src/attribute/landmark.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SemanticNode } from '../format/snapshot.js';\nimport { UNATTRIBUTED } from './boundary.js';\n\n/**\n * Where each thing on a subject was, kept as the run read it.\n *\n * The bags next door say what a subject holds; these say where it held it, off\n * the same walk over the same tree, because two walks eventually disagree about\n * what was there. A node earns a place by bearing a role, an accessible name,\n * or words of its own — never by what it is called — so a screen written under\n * an era's worth of higher-order components reduces to the same landmarks as\n * the same screen written flat.\n */\n\n/** One thing on a subject a person could point at, where the run saw it. */\nexport interface Landmark {\n /** Accessible role, when the reading resolved one. */\n readonly role?: string;\n /** Accessible name — what a screen reader would announce. */\n readonly name?: string;\n /**\n * The words directly inside it, and only those. Text belonging to a nested\n * landmark stays on that landmark, so a container does not inherit the page.\n */\n readonly text?: string;\n /** Index of the nearest enclosing landmark. Absent on a top-level one. */\n readonly within?: number;\n /**\n * `[x, y, width, height]` in layout pixels, integers.\n *\n * Absent, never zeroed, exactly as `rect` is: a reading that did not resolve\n * layout has no opinion about what sits beneath what, and must say so rather\n * than answer from document order while sounding equally sure.\n */\n readonly box?: readonly [number, number, number, number];\n /** Where the element was written, when the project installed the plugin. */\n readonly file?: string;\n readonly line?: number;\n /**\n * The innermost component that owns it, off the fiber's owner chain.\n *\n * The line an element was written at needs the JSX-source plugin, which a\n * production build strips — and a built Storybook is one, so `file` and\n * `line` are absent on exactly the runs that matter most. The owner chain\n * survives that build: not the line, but the component whose source you would\n * open, joined to a path through `LexiconReport.declaredIn`.\n */\n readonly component?: string;\n /** The component whose JSX created it. The unit somebody owns. */\n readonly createdBy?: string;\n /** A test handle, when one was set. The one name a suite chose deliberately. */\n readonly handle?: string;\n}\n\n/**\n * Landmarks kept per subject.\n *\n * Higher than the cap above on purpose. A field that loses its two hundredth\n * distinct value loses a word the subject also says somewhere else; a subject\n * that loses its last landmark loses a *place*, and a place has no second\n * spelling.\n *\n * Four hundred was a guess and a 572-subject application falsified it: p50 9,\n * p90 52, p99 227, and four subjects pinned at the cap, losing 1,973 places\n * between them off screens of roughly 1,179, 926, 836 and 632. Those four are\n * its biggest screens, which are the ones somebody most needs orienting on, and\n * what a cap cuts there is the bottom of the page. Twelve hundred clears the\n * largest with room and leaves the other 539 an order of magnitude below it.\n */\nexport const LANDMARK_CAP = 1_200;\n\n/** A digest is a coordinate, not a word: matching one matches a hash. */\nexport function isDigest(value: string): boolean {\n return /^v\\d+:[0-9a-f]+$/.test(value);\n}\n\n/**\n * Document order, carrying what encloses what.\n *\n * `visit` returns the index a child should call its enclosure, which is how\n * containment survives a tree whose intermediate nodes are nearly all\n * scaffolding: a landmark's `within` names the nearest *landmark* above it, not\n * the nearest node, so the depth of the wrapper stack between them costs\n * nothing.\n */\nexport function walk(\n node: SemanticNode,\n within: number | undefined,\n visit: (node: SemanticNode, within: number | undefined) => number | undefined,\n): void {\n const at = visit(node, within);\n for (const child of node.children) walk(child, at, visit);\n}\n\n/**\n * The landmark this node earns, or nothing.\n *\n * The test is what the node says. A text node's words belong to the element\n * holding them, which was visited first and has already taken them — visited\n * all the same, because the `text` bag is indexed from exactly these.\n */\nexport function landmarkFor(node: SemanticNode, within: number | undefined): Landmark | undefined {\n if (node.tag === '#text') return undefined;\n const text = ownTextOf(node);\n const name = node.name ?? node.description ?? labelOf(node);\n if (node.role === undefined && name === undefined && text === undefined) return undefined;\n return landmarkOf(node, text, name, within);\n}\n\nfunction landmarkOf(\n node: SemanticNode,\n text: string | undefined,\n name: string | undefined,\n within: number | undefined,\n): Landmark {\n const source = node.provenance?.source;\n const handle = node.attributes['data-testid'] ?? node.attributes['data-test-id'];\n // `owners[0]` is the innermost frame: the component that rendered this node,\n // not the one it was placed by. That is the file somebody opens.\n const component = node.provenance?.owners?.[0]?.name;\n\n return {\n ...(node.role === undefined ? {} : { role: node.role }),\n ...(name === undefined ? {} : { name }),\n ...(text === undefined ? {} : { text }),\n ...(within === undefined ? {} : { within }),\n ...(node.rect === undefined\n ? {}\n : {\n box: [\n Math.round(node.rect.x),\n Math.round(node.rect.y),\n Math.round(node.rect.width),\n Math.round(node.rect.height),\n ] as const,\n }),\n ...(source === undefined ? {} : { file: source.file, line: source.line }),\n ...(component === undefined || component === UNATTRIBUTED ? {} : { component }),\n ...(node.provenance?.createdBy === undefined ? {} : { createdBy: node.provenance.createdBy }),\n ...(handle === undefined ? {} : { handle }),\n };\n}\n\n/**\n * The words this node says itself.\n *\n * Direct text children only. A container does not inherit the sentence its\n * button holds, because the question a landmark answers is *where does this\n * sentence live*, and an answer that says \"on the page\" is not one.\n */\nfunction ownTextOf(node: SemanticNode): string | undefined {\n let held = '';\n for (const child of node.children) {\n if (child.tag !== '#text') continue;\n const text = child.text;\n if (text === undefined || isDigest(text)) continue;\n held = held === '' ? text : `${held} ${text}`;\n }\n const trimmed = held.replace(/\\s+/g, ' ').trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/**\n * A name for a node that resolved none, from the attributes that are a label\n * for a person rather than a value for a machine. The same three `names` reads.\n */\nfunction labelOf(node: SemanticNode): string | undefined {\n const held = node.attributes['placeholder'] ?? node.attributes['alt'] ?? node.attributes['title'];\n if (held === undefined) return undefined;\n const trimmed = held.trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/**\n * Which landmarks survive a subject that overran the cap.\n *\n * Words first: a landmark with a name or a sentence is one a question can\n * reach, and a bare `generic` with a rectangle is one nothing will ever ask\n * for. Ties keep document order, so what survives is still an arrangement.\n * `within` is rewritten to the nearest surviving enclosure, because an index\n * into a list that has been cut is otherwise a pointer at the wrong thing.\n */\nexport function capLandmarks(landmarks: readonly Landmark[]): readonly Landmark[] {\n const worth = (landmark: Landmark): number =>\n (landmark.name === undefined ? 0 : 2) + (landmark.text === undefined ? 0 : 1);\n\n const keep = landmarks\n .map((landmark, at) => ({ landmark, at }))\n .sort((left, right) => worth(right.landmark) - worth(left.landmark) || left.at - right.at)\n .slice(0, LANDMARK_CAP)\n .sort((left, right) => left.at - right.at);\n\n const moved = new Map<number, number>();\n keep.forEach(({ at }, to) => moved.set(at, to));\n\n const enclosure = (at: number | undefined): number | undefined => {\n let held = at;\n while (held !== undefined) {\n const to = moved.get(held);\n if (to !== undefined) return to;\n held = landmarks[held]?.within;\n }\n return undefined;\n };\n\n return keep.map(({ landmark }) => {\n const within = enclosure(landmark.within);\n const { within: _dropped, ...rest } = landmark;\n return within === undefined ? rest : { ...rest, within };\n });\n}\n"]}