@variance-authority/core 0.1.1 → 0.2.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.
- package/CHANGELOG.md +190 -0
- package/README.md +51 -9
- package/dist/attribute/closure.d.ts +105 -0
- package/dist/attribute/closure.js +180 -0
- package/dist/attribute/closure.js.map +1 -0
- package/dist/attribute/composition.d.ts +5 -3
- package/dist/attribute/composition.js.map +1 -1
- package/dist/attribute/divergence.js +7 -1
- package/dist/attribute/divergence.js.map +1 -1
- package/dist/attribute/index.d.ts +5 -1
- package/dist/attribute/index.js +3 -1
- package/dist/attribute/index.js.map +1 -1
- package/dist/attribute/lexicon.d.ts +88 -0
- package/dist/attribute/lexicon.js +150 -0
- package/dist/attribute/lexicon.js.map +1 -0
- package/dist/attribute/source.d.ts +19 -2
- package/dist/attribute/source.js +12 -0
- package/dist/attribute/source.js.map +1 -1
- package/dist/compare/derive.d.ts +17 -0
- package/dist/compare/derive.js.map +1 -1
- package/dist/compare/diff/index.d.ts +25 -1
- package/dist/compare/diff/index.js +8 -5
- package/dist/compare/diff/index.js.map +1 -1
- package/dist/compare/explain.d.ts +1 -1
- package/dist/compare/explain.js +15 -8
- package/dist/compare/explain.js.map +1 -1
- package/dist/compare/index.d.ts +2 -2
- package/dist/compare/index.js +1 -1
- package/dist/compare/index.js.map +1 -1
- package/dist/compare/observability.d.ts +25 -1
- package/dist/compare/observability.js +54 -0
- package/dist/compare/observability.js.map +1 -1
- package/dist/compare/parting.d.ts +24 -2
- package/dist/compare/parting.js +14 -9
- package/dist/compare/parting.js.map +1 -1
- package/dist/compare/slice.d.ts +15 -2
- package/dist/compare/slice.js +6 -2
- package/dist/compare/slice.js.map +1 -1
- package/dist/format/capture.d.ts +15 -0
- package/dist/format/capture.js +30 -1
- package/dist/format/capture.js.map +1 -1
- package/dist/format/document.d.ts +65 -5
- package/dist/format/document.js +34 -0
- package/dist/format/document.js.map +1 -1
- package/dist/format/hash.d.ts +26 -0
- package/dist/format/hash.js +41 -0
- package/dist/format/hash.js.map +1 -1
- package/dist/format/index.d.ts +4 -3
- package/dist/format/index.js +3 -2
- package/dist/format/index.js.map +1 -1
- package/dist/format/sha256.d.ts +10 -2
- package/dist/format/sha256.js +42 -9
- package/dist/format/sha256.js.map +1 -1
- package/dist/format/stabilize.d.ts +15 -0
- package/dist/format/stabilize.js +22 -1
- package/dist/format/stabilize.js.map +1 -1
- package/dist/index.d.ts +3 -11
- package/dist/index.js +3 -11
- package/dist/index.js.map +1 -1
- package/dist/relate/graph.d.ts +26 -6
- package/dist/relate/graph.js +14 -7
- package/dist/relate/graph.js.map +1 -1
- package/dist/relate/index.d.ts +2 -2
- package/dist/relate/index.js +1 -1
- package/dist/relate/index.js.map +1 -1
- package/dist/relate/merkle.d.ts +8 -5
- package/dist/relate/merkle.js +8 -6
- package/dist/relate/merkle.js.map +1 -1
- package/dist/relate/reach.d.ts +19 -5
- package/dist/relate/reach.js +10 -8
- package/dist/relate/reach.js.map +1 -1
- package/dist/relate/records.d.ts +29 -4
- package/dist/relate/records.js +60 -7
- package/dist/relate/records.js.map +1 -1
- package/dist/segment/index.d.ts +123 -0
- package/dist/segment/index.js +265 -0
- package/dist/segment/index.js.map +1 -0
- package/dist/share/index.d.ts +148 -0
- package/dist/share/index.js +190 -0
- package/dist/share/index.js.map +1 -0
- package/package.json +10 -2
|
@@ -110,9 +110,11 @@ export interface ComponentEntry {
|
|
|
110
110
|
* element. They are the same thing only for a component that authors a node of
|
|
111
111
|
* its own, and a real application is full of components that do not: measured
|
|
112
112
|
* on `examples/todomvc`, every `Chip` reports `within: ['Stack']` and
|
|
113
|
-
* `createdBy: ['TodoFooter']
|
|
114
|
-
*
|
|
115
|
-
*
|
|
113
|
+
* `createdBy: ['TodoFooter']`: the `Stack` that encloses a chip is a layout
|
|
114
|
+
* primitive that knows nothing about it, and the footer that wrote the element
|
|
115
|
+
* stands one boundary further out. A component that renders nothing but other
|
|
116
|
+
* components owns no node, is a boundary nowhere, and appears in this graph
|
|
117
|
+
* *only* here — while being the file a reviewer has to open.
|
|
116
118
|
*
|
|
117
119
|
* Empty on a production build, where `_debugOwner` is absent (ADR-0007). Empty
|
|
118
120
|
* is *not* "nothing mounted it": a component with no caller is the subject root
|
|
@@ -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":"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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']`: 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\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"]}
|
|
@@ -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":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAkEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,UAAU,aAAa,CAC3B,UAAqC,EACrC,SAAgD;IAEhD,MAAM,WAAW,GAAiB,EAAE,CAAC;IAErC,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;QAC/B,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS;YACvE,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,UAAU,CAAC;gBAAE,SAAS;YAE9C,yEAAyE;YACzE,uEAAuE;YACvE,yEAAyE;YACzE,4EAA4E;YAC5E,iBAAiB;YACjB,MAAM,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC;YAC1C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAQ,CAAC;YAC9B,KAAK,MAAM,KAAK,IAAI,IAAI;gBAAE,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAM,EAAE,KAAK,CAAC;oBAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAExF,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;YAEzD,WAAW,CAAC,IAAI,CAAC;gBACf,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC;gBACjB,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;aAChD,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAS,UAAU,CACjB,UAAgC,EAChC,SAAgD;IAEhD,MAAM,KAAK,GAAG,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACtD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAE1C,MAAM,KAAK,GAAwB,EAAE,CAAC;IACtC,KAAK,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC;QACtD,IAAI,KAAK,KAAK,CAAC;YAAE,SAAS;QAC1B,MAAM,KAAK,GAAG,aAAa,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAClD,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC;IAChG,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,SAAS,aAAa,CACpB,SAAgC,EAChC,SAAgD;IAEhD,MAAM,IAAI,GAAG,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACjC,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC7C,OAAO,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;AAC/C,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,YAAY,CAAC,UAAgC;IACpD,MAAM,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,GAAG,UAAU,CAAC;IACpC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAEtC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAClE,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAEjF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,KAAK,MAAM,OAAO,IAAI,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YAC3E,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;gBAAE,OAAO,KAAK,CAAC;YACpC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,SAAS,CAAC,CAAoB,EAAE,CAAoB;IAC3D,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;GAGG;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 { 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,13 @@ 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 type { BoundaryRow, LexiconField, LexiconOptions, SubjectLexicon } from './lexicon.js';
|
|
37
41
|
export { attributeMovement } from './movement.js';
|
|
38
42
|
export type { Attribution, Cause, Evidence, Moved, Movement } from './movement.js';
|
|
39
43
|
export { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';
|
package/dist/attribute/index.js
CHANGED
|
@@ -16,13 +16,15 @@
|
|
|
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';
|
|
26
28
|
export { attributeMovement } from './movement.js';
|
|
27
29
|
export { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';
|
|
28
30
|
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,
|
|
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;AAGnE,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 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,88 @@
|
|
|
1
|
+
import type { SubjectComposition } from './composition.js';
|
|
2
|
+
/**
|
|
3
|
+
* The subject-first folds: every name a subject carries, and its tree as rows.
|
|
4
|
+
*
|
|
5
|
+
* `composeSubjects` folds the suite component-first, which is the axis a
|
|
6
|
+
* *finding* wants: which component, in how many subjects, mounted by whom. The
|
|
7
|
+
* question an agent asks on arrival runs the other way: *which subject is the
|
|
8
|
+
* one I mean*, and *what is that subject made of*. Both are one pass over the
|
|
9
|
+
* same instance lists, done here so that whoever holds the lists, the run or a
|
|
10
|
+
* test holding snapshots, gets the same rows the report carries.
|
|
11
|
+
*
|
|
12
|
+
* Neither fold ranks anything. The lexicon is values per field and the structure
|
|
13
|
+
* is rows in document order; what a reader does with them is the reader's rule,
|
|
14
|
+
* and it is printed where it is applied.
|
|
15
|
+
*/
|
|
16
|
+
/** The vocabularies a subject is indexed under. Mirrors the report's `LexiconField`. */
|
|
17
|
+
export type LexiconField = 'example' | 'names' | 'text' | 'components' | 'createdBy' | 'regions' | 'files' | 'roles' | 'tokens';
|
|
18
|
+
/** One subject's names, per field. The shape the report carries verbatim. */
|
|
19
|
+
export interface SubjectLexicon {
|
|
20
|
+
readonly subject: string;
|
|
21
|
+
/** Attributed boundaries. The only number a rank may read. */
|
|
22
|
+
readonly boundaries: number;
|
|
23
|
+
/** Distinct values per field, code-unit sorted; a field found empty is absent. */
|
|
24
|
+
readonly terms: Partial<Record<LexiconField, readonly string[]>>;
|
|
25
|
+
/** Distinct values a field's cap left out, where it cut. */
|
|
26
|
+
readonly elided?: Partial<Record<LexiconField, number>>;
|
|
27
|
+
}
|
|
28
|
+
/** What the fold cannot read off the instances and the snapshot. */
|
|
29
|
+
export interface LexiconOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Subject → the components it is the narrow example of, from the composition.
|
|
32
|
+
* Taken rather than re-derived so the lexicon and the census cannot disagree
|
|
33
|
+
* about which story shows what.
|
|
34
|
+
*/
|
|
35
|
+
readonly examples?: ReadonlyMap<string, readonly string[]>;
|
|
36
|
+
/** Component → files declaring it, from the source index. */
|
|
37
|
+
readonly declaredIn?: ReadonlyMap<string, readonly string[]>;
|
|
38
|
+
/** Subject → lexical names of the regions its journey entered, from the journal. */
|
|
39
|
+
readonly regions?: ReadonlyMap<string, readonly string[]>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Distinct values kept per field. Generous, and what it cuts is counted in
|
|
43
|
+
* `elided`, because a cap that says nothing reads as coverage.
|
|
44
|
+
*/
|
|
45
|
+
export declare const LEXICON_CAP = 200;
|
|
46
|
+
/**
|
|
47
|
+
* Every name each subject carries, per field.
|
|
48
|
+
*
|
|
49
|
+
* Fields come from three places and the option that supplies each says which:
|
|
50
|
+
* the instances give `components`, `createdBy` and `tokens`; the snapshot, when
|
|
51
|
+
* the subject carries one, gives `roles`, `names`, `text` and the call-site
|
|
52
|
+
* `files`; the options give `example`, declared `files` and `regions`. A subject
|
|
53
|
+
* with no snapshot still indexes under what its instances hold, which is the
|
|
54
|
+
* honest shape for a sidecar read back without its document.
|
|
55
|
+
*
|
|
56
|
+
* Digests are never values. A text the policy declared volatile reaches the
|
|
57
|
+
* snapshot as `v1:…`, and a reader that matched on it would be matching a
|
|
58
|
+
* coordinate, not a word.
|
|
59
|
+
*
|
|
60
|
+
* Values are kept as they were read and never split. Splitting is a rule, and a
|
|
61
|
+
* rule applied here would be applied to one side of a later comparison only —
|
|
62
|
+
* the reader's query would meet tokens it did not produce. So the fold stores
|
|
63
|
+
* words and the reader owns the rule, which is how a term and a value are
|
|
64
|
+
* always measured by the same instrument.
|
|
65
|
+
*/
|
|
66
|
+
export declare function lexiconOf(subjects: readonly SubjectComposition[], options?: LexiconOptions): readonly SubjectLexicon[];
|
|
67
|
+
/** Boundaries of one component at one place in a subject's tree. */
|
|
68
|
+
export interface BoundaryRow {
|
|
69
|
+
readonly component: string;
|
|
70
|
+
readonly depth: number;
|
|
71
|
+
readonly within?: string;
|
|
72
|
+
readonly createdBy?: string;
|
|
73
|
+
/** Boundaries folded into this row. */
|
|
74
|
+
readonly count: number;
|
|
75
|
+
/** Distinct props digests among them; unknown props count as one. */
|
|
76
|
+
readonly variants: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* One subject's boundaries as rows, document order, identical rows folded.
|
|
80
|
+
*
|
|
81
|
+
* A row's key is where it sits and who put it there: component, depth,
|
|
82
|
+
* enclosing boundary, creator. Not what it rendered, because the question this
|
|
83
|
+
* answers is *what is this subject made of* and three chips under one stack are
|
|
84
|
+
* one answer to it. What differed among them survives as `variants`, so the
|
|
85
|
+
* fold loses the digests and keeps the fact that there were several.
|
|
86
|
+
*/
|
|
87
|
+
export declare function structureOf(subject: SubjectComposition): readonly BoundaryRow[];
|
|
88
|
+
//# sourceMappingURL=lexicon.d.ts.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { attributed } from './instances.js';
|
|
2
|
+
/**
|
|
3
|
+
* Distinct values kept per field. Generous, and what it cuts is counted in
|
|
4
|
+
* `elided`, because a cap that says nothing reads as coverage.
|
|
5
|
+
*/
|
|
6
|
+
export const LEXICON_CAP = 200;
|
|
7
|
+
/**
|
|
8
|
+
* Every name each subject carries, per field.
|
|
9
|
+
*
|
|
10
|
+
* Fields come from three places and the option that supplies each says which:
|
|
11
|
+
* the instances give `components`, `createdBy` and `tokens`; the snapshot, when
|
|
12
|
+
* the subject carries one, gives `roles`, `names`, `text` and the call-site
|
|
13
|
+
* `files`; the options give `example`, declared `files` and `regions`. A subject
|
|
14
|
+
* with no snapshot still indexes under what its instances hold, which is the
|
|
15
|
+
* honest shape for a sidecar read back without its document.
|
|
16
|
+
*
|
|
17
|
+
* Digests are never values. A text the policy declared volatile reaches the
|
|
18
|
+
* snapshot as `v1:…`, and a reader that matched on it would be matching a
|
|
19
|
+
* coordinate, not a word.
|
|
20
|
+
*
|
|
21
|
+
* Values are kept as they were read and never split. Splitting is a rule, and a
|
|
22
|
+
* rule applied here would be applied to one side of a later comparison only —
|
|
23
|
+
* the reader's query would meet tokens it did not produce. So the fold stores
|
|
24
|
+
* words and the reader owns the rule, which is how a term and a value are
|
|
25
|
+
* always measured by the same instrument.
|
|
26
|
+
*/
|
|
27
|
+
export function lexiconOf(subjects, options = {}) {
|
|
28
|
+
return subjects.map((subject) => {
|
|
29
|
+
const fields = new Map();
|
|
30
|
+
const add = (field, value) => {
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return;
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
if (trimmed === '' || isDigest(trimmed))
|
|
35
|
+
return;
|
|
36
|
+
let set = fields.get(field);
|
|
37
|
+
if (set === undefined)
|
|
38
|
+
fields.set(field, (set = new Set()));
|
|
39
|
+
set.add(trimmed);
|
|
40
|
+
};
|
|
41
|
+
const held = subject.instances.filter(attributed);
|
|
42
|
+
for (const instance of held) {
|
|
43
|
+
add('components', instance.component);
|
|
44
|
+
add('createdBy', instance.createdBy);
|
|
45
|
+
for (const token of instance.tokens)
|
|
46
|
+
add('tokens', token);
|
|
47
|
+
for (const file of options.declaredIn?.get(instance.component) ?? [])
|
|
48
|
+
add('files', file);
|
|
49
|
+
}
|
|
50
|
+
for (const component of options.examples?.get(subject.subject) ?? [])
|
|
51
|
+
add('example', component);
|
|
52
|
+
for (const region of options.regions?.get(subject.subject) ?? [])
|
|
53
|
+
add('regions', region);
|
|
54
|
+
if (subject.snapshot !== undefined) {
|
|
55
|
+
walk(subject.snapshot.root, (node) => {
|
|
56
|
+
add('roles', node.role);
|
|
57
|
+
add('names', node.name);
|
|
58
|
+
add('names', node.description);
|
|
59
|
+
// The three attributes that are a label for a person rather than a
|
|
60
|
+
// value for a machine. `value` is state, `href` is a coordinate, and
|
|
61
|
+
// `class` never survives normalization at all.
|
|
62
|
+
add('names', node.attributes['placeholder']);
|
|
63
|
+
add('names', node.attributes['alt']);
|
|
64
|
+
add('names', node.attributes['title']);
|
|
65
|
+
add('text', node.text);
|
|
66
|
+
add('files', node.provenance?.source?.file);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const terms = {};
|
|
70
|
+
const elided = {};
|
|
71
|
+
for (const field of FIELDS) {
|
|
72
|
+
const values = fields.get(field);
|
|
73
|
+
if (values === undefined || values.size === 0)
|
|
74
|
+
continue;
|
|
75
|
+
const sorted = [...values].sort(codeUnit);
|
|
76
|
+
terms[field] = sorted.slice(0, LEXICON_CAP);
|
|
77
|
+
if (sorted.length > LEXICON_CAP)
|
|
78
|
+
elided[field] = sorted.length - LEXICON_CAP;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
subject: subject.subject,
|
|
82
|
+
boundaries: held.length,
|
|
83
|
+
terms,
|
|
84
|
+
...(Object.keys(elided).length === 0 ? {} : { elided }),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* One subject's boundaries as rows, document order, identical rows folded.
|
|
90
|
+
*
|
|
91
|
+
* A row's key is where it sits and who put it there: component, depth,
|
|
92
|
+
* enclosing boundary, creator. Not what it rendered, because the question this
|
|
93
|
+
* answers is *what is this subject made of* and three chips under one stack are
|
|
94
|
+
* one answer to it. What differed among them survives as `variants`, so the
|
|
95
|
+
* fold loses the digests and keeps the fact that there were several.
|
|
96
|
+
*/
|
|
97
|
+
export function structureOf(subject) {
|
|
98
|
+
const rows = new Map();
|
|
99
|
+
for (const instance of subject.instances) {
|
|
100
|
+
if (!attributed(instance))
|
|
101
|
+
continue;
|
|
102
|
+
const key = [instance.component, instance.depth, instance.within ?? '', instance.createdBy ?? '']
|
|
103
|
+
.join(' ');
|
|
104
|
+
const props = instance.props ?? UNKNOWN;
|
|
105
|
+
const held = rows.get(key);
|
|
106
|
+
if (held === undefined) {
|
|
107
|
+
rows.set(key, { row: rowOf(instance), props: new Set([props]) });
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
held.props.add(props);
|
|
111
|
+
held.row = { ...held.row, count: held.row.count + 1 };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [...rows.values()].map(({ row, props }) => ({ ...row, variants: props.size }));
|
|
115
|
+
}
|
|
116
|
+
function rowOf(instance) {
|
|
117
|
+
return {
|
|
118
|
+
component: instance.component,
|
|
119
|
+
depth: instance.depth,
|
|
120
|
+
...(instance.within === undefined ? {} : { within: instance.within }),
|
|
121
|
+
...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),
|
|
122
|
+
count: 1,
|
|
123
|
+
variants: 1,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const FIELDS = [
|
|
127
|
+
'example',
|
|
128
|
+
'names',
|
|
129
|
+
'text',
|
|
130
|
+
'components',
|
|
131
|
+
'createdBy',
|
|
132
|
+
'regions',
|
|
133
|
+
'files',
|
|
134
|
+
'roles',
|
|
135
|
+
'tokens',
|
|
136
|
+
];
|
|
137
|
+
const UNKNOWN = '(unknown props)';
|
|
138
|
+
function isDigest(value) {
|
|
139
|
+
return /^v\d+:[0-9a-f]+$/.test(value);
|
|
140
|
+
}
|
|
141
|
+
function walk(node, visit) {
|
|
142
|
+
visit(node);
|
|
143
|
+
for (const child of node.children)
|
|
144
|
+
walk(child, visit);
|
|
145
|
+
}
|
|
146
|
+
/** Code-unit order: the same bytes on every machine, whatever `LANG` says. */
|
|
147
|
+
function codeUnit(left, right) {
|
|
148
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=lexicon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lexicon.js","sourceRoot":"","sources":["../../src/attribute/lexicon.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SemanticNode } from '../format/snapshot.js';\nimport { attributed, type ComponentInstance } from './instances.js';\nimport type { SubjectComposition } from './composition.js';\n\n/**\n * The subject-first folds: every name a subject carries, and its tree as rows.\n *\n * `composeSubjects` folds the suite component-first, which is the axis a\n * *finding* wants: which component, in how many subjects, mounted by whom. The\n * question an agent asks on arrival runs the other way: *which subject is the\n * one I mean*, and *what is that subject made of*. Both are one pass over the\n * same instance lists, done here so that whoever holds the lists, the run or a\n * test holding snapshots, gets the same rows the report carries.\n *\n * Neither fold ranks anything. The lexicon is values per field and the structure\n * is rows in document order; what a reader does with them is the reader's rule,\n * and it is printed where it is applied.\n */\n\n/** The vocabularies a subject is indexed under. Mirrors the report's `LexiconField`. */\nexport type LexiconField =\n | 'example'\n | 'names'\n | 'text'\n | 'components'\n | 'createdBy'\n | 'regions'\n | 'files'\n | 'roles'\n | 'tokens';\n\n/** One subject's names, per field. The shape the report carries verbatim. */\nexport interface SubjectLexicon {\n readonly subject: string;\n /** Attributed boundaries. The only number a rank may read. */\n readonly boundaries: number;\n /** Distinct values per field, code-unit sorted; a field found empty is absent. */\n readonly terms: Partial<Record<LexiconField, readonly string[]>>;\n /** Distinct values a field's cap left out, where it cut. */\n readonly elided?: Partial<Record<LexiconField, number>>;\n}\n\n/** What the fold cannot read off the instances and the snapshot. */\nexport interface LexiconOptions {\n /**\n * Subject → the components it is the narrow example of, from the composition.\n * Taken rather than re-derived so the lexicon and the census cannot disagree\n * about which story shows what.\n */\n readonly examples?: ReadonlyMap<string, readonly string[]>;\n /** Component → files declaring it, from the source index. */\n readonly declaredIn?: ReadonlyMap<string, readonly string[]>;\n /** Subject → lexical names of the regions its journey entered, from the journal. */\n readonly regions?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * Distinct values kept per field. Generous, and what it cuts is counted in\n * `elided`, because a cap that says nothing reads as coverage.\n */\nexport const LEXICON_CAP = 200;\n\n/**\n * Every name each subject carries, per field.\n *\n * Fields come from three places and the option that supplies each says which:\n * the instances give `components`, `createdBy` and `tokens`; the snapshot, when\n * the subject carries one, gives `roles`, `names`, `text` and the call-site\n * `files`; the options give `example`, declared `files` and `regions`. A subject\n * with no snapshot still indexes under what its instances hold, which is the\n * honest shape for a sidecar read back without its document.\n *\n * Digests are never values. A text the policy declared volatile reaches the\n * snapshot as `v1:…`, and a reader that matched on it would be matching a\n * coordinate, not a word.\n *\n * Values are kept as they were read and never split. Splitting is a rule, and a\n * rule applied here would be applied to one side of a later comparison only —\n * the reader's query would meet tokens it did not produce. So the fold stores\n * words and the reader owns the rule, which is how a term and a value are\n * always measured by the same instrument.\n */\nexport function lexiconOf(\n subjects: readonly SubjectComposition[],\n options: LexiconOptions = {},\n): readonly SubjectLexicon[] {\n return subjects.map((subject) => {\n const fields = new Map<LexiconField, Set<string>>();\n const add = (field: LexiconField, value: string | undefined) => {\n if (value === undefined) return;\n const trimmed = value.trim();\n if (trimmed === '' || isDigest(trimmed)) return;\n let set = fields.get(field);\n if (set === undefined) fields.set(field, (set = new Set()));\n set.add(trimmed);\n };\n\n const held = subject.instances.filter(attributed);\n for (const instance of held) {\n add('components', instance.component);\n add('createdBy', instance.createdBy);\n for (const token of instance.tokens) add('tokens', token);\n for (const file of options.declaredIn?.get(instance.component) ?? []) add('files', file);\n }\n for (const component of options.examples?.get(subject.subject) ?? []) add('example', component);\n for (const region of options.regions?.get(subject.subject) ?? []) add('regions', region);\n\n if (subject.snapshot !== undefined) {\n walk(subject.snapshot.root, (node) => {\n add('roles', node.role);\n add('names', node.name);\n add('names', node.description);\n // The three attributes that are a label for a person rather than a\n // value for a machine. `value` is state, `href` is a coordinate, and\n // `class` never survives normalization at all.\n add('names', node.attributes['placeholder']);\n add('names', node.attributes['alt']);\n add('names', node.attributes['title']);\n add('text', node.text);\n add('files', node.provenance?.source?.file);\n });\n }\n\n const terms: Partial<Record<LexiconField, readonly string[]>> = {};\n const elided: Partial<Record<LexiconField, number>> = {};\n for (const field of FIELDS) {\n const values = fields.get(field);\n if (values === undefined || values.size === 0) continue;\n const sorted = [...values].sort(codeUnit);\n terms[field] = sorted.slice(0, LEXICON_CAP);\n if (sorted.length > LEXICON_CAP) elided[field] = sorted.length - LEXICON_CAP;\n }\n\n return {\n subject: subject.subject,\n boundaries: held.length,\n terms,\n ...(Object.keys(elided).length === 0 ? {} : { elided }),\n };\n });\n}\n\n/** Boundaries of one component at one place in a subject's tree. */\nexport interface BoundaryRow {\n readonly component: string;\n readonly depth: number;\n readonly within?: string;\n readonly createdBy?: string;\n /** Boundaries folded into this row. */\n readonly count: number;\n /** Distinct props digests among them; unknown props count as one. */\n readonly variants: number;\n}\n\n/**\n * One subject's boundaries as rows, document order, identical rows folded.\n *\n * A row's key is where it sits and who put it there: component, depth,\n * enclosing boundary, creator. Not what it rendered, because the question this\n * answers is *what is this subject made of* and three chips under one stack are\n * one answer to it. What differed among them survives as `variants`, so the\n * fold loses the digests and keeps the fact that there were several.\n */\nexport function structureOf(subject: SubjectComposition): readonly BoundaryRow[] {\n const rows = new Map<string, { row: BoundaryRow; props: Set<string> }>();\n\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n const key = [instance.component, instance.depth, instance.within ?? '', instance.createdBy ?? '']\n .join(' ');\n const props = instance.props ?? UNKNOWN;\n const held = rows.get(key);\n if (held === undefined) {\n rows.set(key, { row: rowOf(instance), props: new Set([props]) });\n } else {\n held.props.add(props);\n held.row = { ...held.row, count: held.row.count + 1 };\n }\n }\n\n return [...rows.values()].map(({ row, props }) => ({ ...row, variants: props.size }));\n}\n\nfunction rowOf(instance: ComponentInstance): BoundaryRow {\n return {\n component: instance.component,\n depth: instance.depth,\n ...(instance.within === undefined ? {} : { within: instance.within }),\n ...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),\n count: 1,\n variants: 1,\n };\n}\n\nconst FIELDS: readonly LexiconField[] = [\n 'example',\n 'names',\n 'text',\n 'components',\n 'createdBy',\n 'regions',\n 'files',\n 'roles',\n 'tokens',\n];\n\nconst UNKNOWN = '(unknown props)';\n\nfunction isDigest(value: string): boolean {\n return /^v\\d+:[0-9a-f]+$/.test(value);\n}\n\nfunction walk(node: SemanticNode, visit: (node: SemanticNode) => void): void {\n visit(node);\n for (const child of node.children) walk(child, visit);\n}\n\n/** Code-unit order: the same bytes on every machine, whatever `LANG` says. */\nfunction codeUnit(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"]}
|
|
@@ -28,8 +28,15 @@ export interface SourceRef {
|
|
|
28
28
|
/** Repository-relative, so a report is portable between machines and CI. */
|
|
29
29
|
readonly file: string;
|
|
30
30
|
readonly line: number;
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* How the declaration was recognised. Carried so a bad match is debuggable.
|
|
33
|
+
*
|
|
34
|
+
* The first four are shapes a scan of the source text matched by name.
|
|
35
|
+
* `engine` is the one that is not a match at all: the browser was asked where
|
|
36
|
+
* the function it rendered was compiled from, and that position was mapped
|
|
37
|
+
* back to the file. It names the component that ran, whatever it was called.
|
|
38
|
+
*/
|
|
39
|
+
readonly via: 'function' | 'const' | 'class' | 'declared' | 'engine';
|
|
33
40
|
}
|
|
34
41
|
/**
|
|
35
42
|
* Component name → where it is declared.
|
|
@@ -74,4 +81,14 @@ export declare function formatSource(resolution: Resolution): string;
|
|
|
74
81
|
export declare function indexSource(file: string, contents: string): SourceIndex;
|
|
75
82
|
/** Merge per-file indexes. A name declared in several files keeps every ref. */
|
|
76
83
|
export declare function mergeSourceIndexes(indexes: readonly SourceIndex[]): SourceIndex;
|
|
84
|
+
/**
|
|
85
|
+
* One index laid over another: a name the overlay holds replaces the base's refs.
|
|
86
|
+
*
|
|
87
|
+
* Not a merge. A scan finds every file that declares a `Button`, and reports the
|
|
88
|
+
* pair as ambiguous because the text cannot say which one a page rendered. The
|
|
89
|
+
* engine can, and when it has answered for a name the scan's guesses for that
|
|
90
|
+
* name are no longer the question. A name the overlay never saw keeps what the
|
|
91
|
+
* base had, so a component the page did not mount still has a file.
|
|
92
|
+
*/
|
|
93
|
+
export declare function overlaySourceIndex(base: SourceIndex, overlay: SourceIndex): SourceIndex;
|
|
77
94
|
//# sourceMappingURL=source.d.ts.map
|
package/dist/attribute/source.js
CHANGED
|
@@ -92,4 +92,16 @@ export function mergeSourceIndexes(indexes) {
|
|
|
92
92
|
}
|
|
93
93
|
return merged;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* One index laid over another: a name the overlay holds replaces the base's refs.
|
|
97
|
+
*
|
|
98
|
+
* Not a merge. A scan finds every file that declares a `Button`, and reports the
|
|
99
|
+
* pair as ambiguous because the text cannot say which one a page rendered. The
|
|
100
|
+
* engine can, and when it has answered for a name the scan's guesses for that
|
|
101
|
+
* name are no longer the question. A name the overlay never saw keeps what the
|
|
102
|
+
* base had, so a component the page did not mount still has a file.
|
|
103
|
+
*/
|
|
104
|
+
export function overlaySourceIndex(base, overlay) {
|
|
105
|
+
return { ...base, ...overlay };
|
|
106
|
+
}
|
|
95
107
|
//# sourceMappingURL=source.js.map
|