@variance-authority/core 0.1.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 +79 -0
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/artifact.d.ts +47 -0
- package/dist/artifact.js +2 -0
- package/dist/artifact.js.map +1 -0
- package/dist/attribute/because.d.ts +43 -0
- package/dist/attribute/because.js +64 -0
- package/dist/attribute/because.js.map +1 -0
- package/dist/attribute/boundary.d.ts +191 -0
- package/dist/attribute/boundary.js +328 -0
- package/dist/attribute/boundary.js.map +1 -0
- package/dist/attribute/call-site.d.ts +96 -0
- package/dist/attribute/call-site.js +183 -0
- package/dist/attribute/call-site.js.map +1 -0
- package/dist/attribute/component-hash.d.ts +188 -0
- package/dist/attribute/component-hash.js +312 -0
- package/dist/attribute/component-hash.js.map +1 -0
- package/dist/attribute/composition-fixture.d.ts +18 -0
- package/dist/attribute/composition-fixture.js +53 -0
- package/dist/attribute/composition-fixture.js.map +1 -0
- package/dist/attribute/composition.d.ts +182 -0
- package/dist/attribute/composition.js +169 -0
- package/dist/attribute/composition.js.map +1 -0
- package/dist/attribute/control.d.ts +43 -0
- package/dist/attribute/control.js +54 -0
- package/dist/attribute/control.js.map +1 -0
- package/dist/attribute/coverage.d.ts +99 -0
- package/dist/attribute/coverage.js +86 -0
- package/dist/attribute/coverage.js.map +1 -0
- package/dist/attribute/divergence.d.ts +102 -0
- package/dist/attribute/divergence.js +152 -0
- package/dist/attribute/divergence.js.map +1 -0
- package/dist/attribute/index.d.ts +43 -0
- package/dist/attribute/index.js +29 -0
- package/dist/attribute/index.js.map +1 -0
- package/dist/attribute/instability.d.ts +89 -0
- package/dist/attribute/instability.js +183 -0
- package/dist/attribute/instability.js.map +1 -0
- package/dist/attribute/instances.d.ts +145 -0
- package/dist/attribute/instances.js +140 -0
- package/dist/attribute/instances.js.map +1 -0
- package/dist/attribute/locate.d.ts +22 -0
- package/dist/attribute/locate.js +100 -0
- package/dist/attribute/locate.js.map +1 -0
- package/dist/attribute/mask.d.ts +156 -0
- package/dist/attribute/mask.js +201 -0
- package/dist/attribute/mask.js.map +1 -0
- package/dist/attribute/movement.d.ts +211 -0
- package/dist/attribute/movement.js +181 -0
- package/dist/attribute/movement.js.map +1 -0
- package/dist/attribute/region.d.ts +188 -0
- package/dist/attribute/region.js +144 -0
- package/dist/attribute/region.js.map +1 -0
- package/dist/attribute/source-map.d.ts +98 -0
- package/dist/attribute/source-map.js +264 -0
- package/dist/attribute/source-map.js.map +1 -0
- package/dist/attribute/source.d.ts +77 -0
- package/dist/attribute/source.js +95 -0
- package/dist/attribute/source.js.map +1 -0
- package/dist/attribute/stack.d.ts +80 -0
- package/dist/attribute/stack.js +108 -0
- package/dist/attribute/stack.js.map +1 -0
- package/dist/compare/band.d.ts +85 -0
- package/dist/compare/band.js +70 -0
- package/dist/compare/band.js.map +1 -0
- package/dist/compare/cascade.d.ts +45 -0
- package/dist/compare/cascade.js +71 -0
- package/dist/compare/cascade.js.map +1 -0
- package/dist/compare/derive.d.ts +78 -0
- package/dist/compare/derive.js +39 -0
- package/dist/compare/derive.js.map +1 -0
- package/dist/compare/diff/attribution.d.ts +23 -0
- package/dist/compare/diff/attribution.js +244 -0
- package/dist/compare/diff/attribution.js.map +1 -0
- package/dist/compare/diff/compare-nodes.d.ts +15 -0
- package/dist/compare/diff/compare-nodes.js +227 -0
- package/dist/compare/diff/compare-nodes.js.map +1 -0
- package/dist/compare/diff/components.d.ts +20 -0
- package/dist/compare/diff/components.js +76 -0
- package/dist/compare/diff/components.js.map +1 -0
- package/dist/compare/diff/delta.d.ts +165 -0
- package/dist/compare/diff/delta.js +12 -0
- package/dist/compare/diff/delta.js.map +1 -0
- package/dist/compare/diff/index.d.ts +69 -0
- package/dist/compare/diff/index.js +101 -0
- package/dist/compare/diff/index.js.map +1 -0
- package/dist/compare/diff/match.d.ts +37 -0
- package/dist/compare/diff/match.js +167 -0
- package/dist/compare/diff/match.js.map +1 -0
- package/dist/compare/explain.d.ts +23 -0
- package/dist/compare/explain.js +190 -0
- package/dist/compare/explain.js.map +1 -0
- package/dist/compare/holding-diff.d.ts +25 -0
- package/dist/compare/holding-diff.js +74 -0
- package/dist/compare/holding-diff.js.map +1 -0
- package/dist/compare/impact.d.ts +56 -0
- package/dist/compare/impact.js +124 -0
- package/dist/compare/impact.js.map +1 -0
- package/dist/compare/index.d.ts +30 -0
- package/dist/compare/index.js +22 -0
- package/dist/compare/index.js.map +1 -0
- package/dist/compare/instance.d.ts +30 -0
- package/dist/compare/instance.js +145 -0
- package/dist/compare/instance.js.map +1 -0
- package/dist/compare/observability.d.ts +49 -0
- package/dist/compare/observability.js +57 -0
- package/dist/compare/observability.js.map +1 -0
- package/dist/compare/parting.d.ts +179 -0
- package/dist/compare/parting.js +244 -0
- package/dist/compare/parting.js.map +1 -0
- package/dist/compare/slice.d.ts +74 -0
- package/dist/compare/slice.js +79 -0
- package/dist/compare/slice.js.map +1 -0
- package/dist/compare/value.d.ts +60 -0
- package/dist/compare/value.js +111 -0
- package/dist/compare/value.js.map +1 -0
- package/dist/format/accessibility.d.ts +27 -0
- package/dist/format/accessibility.js +17 -0
- package/dist/format/accessibility.js.map +1 -0
- package/dist/format/canonical.d.ts +38 -0
- package/dist/format/canonical.js +65 -0
- package/dist/format/canonical.js.map +1 -0
- package/dist/format/capture.d.ts +212 -0
- package/dist/format/capture.js +2 -0
- package/dist/format/capture.js.map +1 -0
- package/dist/format/document.d.ts +279 -0
- package/dist/format/document.js +65 -0
- package/dist/format/document.js.map +1 -0
- package/dist/format/environment.d.ts +120 -0
- package/dist/format/environment.js +53 -0
- package/dist/format/environment.js.map +1 -0
- package/dist/format/hash.d.ts +45 -0
- package/dist/format/hash.js +41 -0
- package/dist/format/hash.js.map +1 -0
- package/dist/format/holding.d.ts +134 -0
- package/dist/format/holding.js +2 -0
- package/dist/format/holding.js.map +1 -0
- package/dist/format/index.d.ts +39 -0
- package/dist/format/index.js +25 -0
- package/dist/format/index.js.map +1 -0
- package/dist/format/intervention.d.ts +87 -0
- package/dist/format/intervention.js +11 -0
- package/dist/format/intervention.js.map +1 -0
- package/dist/format/profile.d.ts +59 -0
- package/dist/format/profile.js +66 -0
- package/dist/format/profile.js.map +1 -0
- package/dist/format/provenance.d.ts +179 -0
- package/dist/format/provenance.js +222 -0
- package/dist/format/provenance.js.map +1 -0
- package/dist/format/sha256.d.ts +34 -0
- package/dist/format/sha256.js +152 -0
- package/dist/format/sha256.js.map +1 -0
- package/dist/format/snapshot.d.ts +263 -0
- package/dist/format/snapshot.js +2 -0
- package/dist/format/snapshot.js.map +1 -0
- package/dist/format/stabilize.d.ts +190 -0
- package/dist/format/stabilize.js +333 -0
- package/dist/format/stabilize.js.map +1 -0
- package/dist/format/tier.d.ts +19 -0
- package/dist/format/tier.js +11 -0
- package/dist/format/tier.js.map +1 -0
- package/dist/format/value.d.ts +115 -0
- package/dist/format/value.js +182 -0
- package/dist/format/value.js.map +1 -0
- package/dist/format/wiring.d.ts +115 -0
- package/dist/format/wiring.js +60 -0
- package/dist/format/wiring.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/judge/docket.d.ts +85 -0
- package/dist/judge/docket.js +162 -0
- package/dist/judge/docket.js.map +1 -0
- package/dist/judge/fingerprint.d.ts +53 -0
- package/dist/judge/fingerprint.js +144 -0
- package/dist/judge/fingerprint.js.map +1 -0
- package/dist/judge/ignore.d.ts +206 -0
- package/dist/judge/ignore.js +237 -0
- package/dist/judge/ignore.js.map +1 -0
- package/dist/judge/index.d.ts +44 -0
- package/dist/judge/index.js +34 -0
- package/dist/judge/index.js.map +1 -0
- package/dist/judge/inspect.d.ts +148 -0
- package/dist/judge/inspect.js +208 -0
- package/dist/judge/inspect.js.map +1 -0
- package/dist/judge/intent.d.ts +146 -0
- package/dist/judge/intent.js +150 -0
- package/dist/judge/intent.js.map +1 -0
- package/dist/judge/locale.d.ts +111 -0
- package/dist/judge/locale.js +247 -0
- package/dist/judge/locale.js.map +1 -0
- package/dist/judge/scope.d.ts +41 -0
- package/dist/judge/scope.js +47 -0
- package/dist/judge/scope.js.map +1 -0
- package/dist/judge/sensitivity.d.ts +202 -0
- package/dist/judge/sensitivity.js +152 -0
- package/dist/judge/sensitivity.js.map +1 -0
- package/dist/judge/trail.d.ts +122 -0
- package/dist/judge/trail.js +96 -0
- package/dist/judge/trail.js.map +1 -0
- package/dist/judge/verdict.d.ts +39 -0
- package/dist/judge/verdict.js +32 -0
- package/dist/judge/verdict.js.map +1 -0
- package/dist/plan/index.d.ts +12 -0
- package/dist/plan/index.js +11 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/plan/tool.d.ts +85 -0
- package/dist/plan/tool.js +87 -0
- package/dist/plan/tool.js.map +1 -0
- package/dist/relate/graph.d.ts +190 -0
- package/dist/relate/graph.js +234 -0
- package/dist/relate/graph.js.map +1 -0
- package/dist/relate/index.d.ts +16 -0
- package/dist/relate/index.js +16 -0
- package/dist/relate/index.js.map +1 -0
- package/dist/relate/merkle.d.ts +110 -0
- package/dist/relate/merkle.js +246 -0
- package/dist/relate/merkle.js.map +1 -0
- package/dist/relate/reach.d.ts +65 -0
- package/dist/relate/reach.js +111 -0
- package/dist/relate/reach.js.map +1 -0
- package/dist/relate/records.d.ts +129 -0
- package/dist/relate/records.js +112 -0
- package/dist/relate/records.js.map +1 -0
- package/dist/rules/index.d.ts +19 -0
- package/dist/rules/index.js +17 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/normalize/alias.d.ts +50 -0
- package/dist/rules/normalize/alias.js +111 -0
- package/dist/rules/normalize/alias.js.map +1 -0
- package/dist/rules/normalize/cascade.d.ts +85 -0
- package/dist/rules/normalize/cascade.js +324 -0
- package/dist/rules/normalize/cascade.js.map +1 -0
- package/dist/rules/normalize/color.d.ts +30 -0
- package/dist/rules/normalize/color.js +185 -0
- package/dist/rules/normalize/color.js.map +1 -0
- package/dist/rules/normalize/fixture.d.ts +83 -0
- package/dist/rules/normalize/fixture.js +74 -0
- package/dist/rules/normalize/fixture.js.map +1 -0
- package/dist/rules/normalize/index.d.ts +39 -0
- package/dist/rules/normalize/index.js +268 -0
- package/dist/rules/normalize/index.js.map +1 -0
- package/dist/rules/normalize/project.d.ts +42 -0
- package/dist/rules/normalize/project.js +83 -0
- package/dist/rules/normalize/project.js.map +1 -0
- package/dist/rules/normalize/shorthand.d.ts +38 -0
- package/dist/rules/normalize/shorthand.js +388 -0
- package/dist/rules/normalize/shorthand.js.map +1 -0
- package/dist/rules/normalize/value.d.ts +23 -0
- package/dist/rules/normalize/value.js +162 -0
- package/dist/rules/normalize/value.js.map +1 -0
- package/dist/rules/normalize/wrapper.d.ts +16 -0
- package/dist/rules/normalize/wrapper.js +137 -0
- package/dist/rules/normalize/wrapper.js.map +1 -0
- package/dist/rules/ruleset.d.ts +93 -0
- package/dist/rules/ruleset.js +175 -0
- package/dist/rules/ruleset.js.map +1 -0
- package/mark.svg +30 -0
- package/package.json +69 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { aggregateImpact } from '../impact.js';
|
|
2
|
+
import { impactTag } from './delta.js';
|
|
3
|
+
/**
|
|
4
|
+
* The component roll-up: who caused this change set, and who merely renders it.
|
|
5
|
+
*
|
|
6
|
+
* Kept apart from attribution because it runs *after* it and asks a different
|
|
7
|
+
* question. Attribution decides what one sentence explains the deltas;
|
|
8
|
+
* this decides which components that sentence implicates, and in what capacity.
|
|
9
|
+
* The two share only the roots, which is exactly the argument for one being able
|
|
10
|
+
* to change without disturbing the other.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Which components a change set implicates, and in what capacity.
|
|
14
|
+
*
|
|
15
|
+
* A component is a *root* when a root's own deltas name it innermost — the change
|
|
16
|
+
* originated there. It is *collateral* when it only ever appears further out in
|
|
17
|
+
* an owner chain, or under a token root: it renders something that changed, but
|
|
18
|
+
* nothing about it changed.
|
|
19
|
+
*/
|
|
20
|
+
export function componentsOf(deltas, roots) {
|
|
21
|
+
const rootNames = new Set();
|
|
22
|
+
for (const root of roots) {
|
|
23
|
+
if (root.kind !== 'component' && root.kind !== 'prop')
|
|
24
|
+
continue;
|
|
25
|
+
// The root's own `cause`, not the innermost owner of its deltas. For a
|
|
26
|
+
// `prop` root those are different components and the innermost is the wrong
|
|
27
|
+
// one: `Panel → Button` means `Panel` made the edit. Falling back to the
|
|
28
|
+
// innermost keeps the previous behaviour where no cause was recorded.
|
|
29
|
+
if (root.cause !== undefined) {
|
|
30
|
+
rootNames.add(root.cause);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
for (const delta of root.deltas) {
|
|
34
|
+
const innermost = delta.owners?.[0]?.name;
|
|
35
|
+
if (innermost !== undefined)
|
|
36
|
+
rootNames.add(innermost);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const accumulator = new Map();
|
|
40
|
+
for (const delta of deltas) {
|
|
41
|
+
const owners = delta.owners ?? [];
|
|
42
|
+
for (const [index, owner] of owners.entries()) {
|
|
43
|
+
let entry = accumulator.get(owner.name);
|
|
44
|
+
if (!entry) {
|
|
45
|
+
entry = { deltas: 0, bands: new Set(), impacts: [], within: new Set() };
|
|
46
|
+
accumulator.set(owner.name, entry);
|
|
47
|
+
}
|
|
48
|
+
// Only the innermost owner is credited with the delta. Every enclosing
|
|
49
|
+
// component would otherwise accumulate every delta beneath it, and a page
|
|
50
|
+
// component would be the biggest change in every diff, every time.
|
|
51
|
+
if (index === 0) {
|
|
52
|
+
entry.deltas += 1;
|
|
53
|
+
entry.bands.add(delta.band);
|
|
54
|
+
entry.impacts.push(impactTag(delta));
|
|
55
|
+
}
|
|
56
|
+
// The chain is innermost-first, so the *next* frame out is what encloses
|
|
57
|
+
// this one. Recording the previous frame instead would answer "what does
|
|
58
|
+
// this component contain?" — which nobody asked, and which reads as an
|
|
59
|
+
// answer to "where does it show up?" until someone checks.
|
|
60
|
+
const enclosing = owners[index + 1];
|
|
61
|
+
if (enclosing)
|
|
62
|
+
entry.within.add(enclosing.name);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return [...accumulator.entries()]
|
|
66
|
+
.map(([name, entry]) => ({
|
|
67
|
+
name,
|
|
68
|
+
role: rootNames.has(name) ? 'root' : 'collateral',
|
|
69
|
+
deltaCount: entry.deltas,
|
|
70
|
+
bands: [...entry.bands],
|
|
71
|
+
impact: aggregateImpact(entry.impacts),
|
|
72
|
+
renderedIn: [...entry.within],
|
|
73
|
+
}))
|
|
74
|
+
.sort((a, b) => b.deltaCount - a.deltaCount || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=components.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"components.js","sourceRoot":"","sources":["../../../src/compare/diff/components.ts"],"names":[],"mappings":"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","sourcesContent":["import { aggregateImpact, type PropertyImpact } from '../impact.js';\nimport type { Band } from '../band.js';\nimport { impactTag, type ChangedComponent, type Delta, type Root } from './delta.js';\n\n/**\n * The component roll-up: who caused this change set, and who merely renders it.\n *\n * Kept apart from attribution because it runs *after* it and asks a different\n * question. Attribution decides what one sentence explains the deltas;\n * this decides which components that sentence implicates, and in what capacity.\n * The two share only the roots, which is exactly the argument for one being able\n * to change without disturbing the other.\n */\n\n/**\n * Which components a change set implicates, and in what capacity.\n *\n * A component is a *root* when a root's own deltas name it innermost — the change\n * originated there. It is *collateral* when it only ever appears further out in\n * an owner chain, or under a token root: it renders something that changed, but\n * nothing about it changed.\n */\nexport function componentsOf(\n deltas: readonly Delta[],\n roots: readonly Root[],\n): readonly ChangedComponent[] {\n const rootNames = new Set<string>();\n for (const root of roots) {\n if (root.kind !== 'component' && root.kind !== 'prop') continue;\n\n // The root's own `cause`, not the innermost owner of its deltas. For a\n // `prop` root those are different components and the innermost is the wrong\n // one: `Panel → Button` means `Panel` made the edit. Falling back to the\n // innermost keeps the previous behaviour where no cause was recorded.\n if (root.cause !== undefined) {\n rootNames.add(root.cause);\n continue;\n }\n\n for (const delta of root.deltas) {\n const innermost = delta.owners?.[0]?.name;\n if (innermost !== undefined) rootNames.add(innermost);\n }\n }\n\n const accumulator = new Map<\n string,\n { deltas: number; bands: Set<Band>; impacts: (PropertyImpact | 'structural')[]; within: Set<string> }\n >();\n\n for (const delta of deltas) {\n const owners = delta.owners ?? [];\n for (const [index, owner] of owners.entries()) {\n let entry = accumulator.get(owner.name);\n if (!entry) {\n entry = { deltas: 0, bands: new Set(), impacts: [], within: new Set() };\n accumulator.set(owner.name, entry);\n }\n\n // Only the innermost owner is credited with the delta. Every enclosing\n // component would otherwise accumulate every delta beneath it, and a page\n // component would be the biggest change in every diff, every time.\n if (index === 0) {\n entry.deltas += 1;\n entry.bands.add(delta.band);\n entry.impacts.push(impactTag(delta));\n }\n\n // The chain is innermost-first, so the *next* frame out is what encloses\n // this one. Recording the previous frame instead would answer \"what does\n // this component contain?\" — which nobody asked, and which reads as an\n // answer to \"where does it show up?\" until someone checks.\n const enclosing = owners[index + 1];\n if (enclosing) entry.within.add(enclosing.name);\n }\n }\n\n return [...accumulator.entries()]\n .map(([name, entry]) => ({\n name,\n role: rootNames.has(name) ? ('root' as const) : ('collateral' as const),\n deltaCount: entry.deltas,\n bands: [...entry.bands],\n impact: aggregateImpact(entry.impacts),\n renderedIn: [...entry.within],\n }))\n .sort(\n (a, b) =>\n b.deltaCount - a.deltaCount || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0),\n );\n}\n"]}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { Band, DeltaKind } from '../band.js';
|
|
2
|
+
import type { AggregateImpact, PropertyImpact } from '../impact.js';
|
|
3
|
+
import type { Rect } from '../../format/capture.js';
|
|
4
|
+
import type { NodePath } from '../../format/snapshot.js';
|
|
5
|
+
import type { OwnerFrame } from '../../format/provenance.js';
|
|
6
|
+
/**
|
|
7
|
+
* A single observed difference.
|
|
8
|
+
*
|
|
9
|
+
* Deltas are the atoms the docket is assembled from. Each one already knows its
|
|
10
|
+
* band and its owner chain when it is created — Principle 5 says causality flows
|
|
11
|
+
* forward, so a change must arrive carrying its name rather than having one
|
|
12
|
+
* reconstructed for it later.
|
|
13
|
+
*/
|
|
14
|
+
export interface Delta {
|
|
15
|
+
readonly kind: DeltaKind;
|
|
16
|
+
readonly band: Band;
|
|
17
|
+
/** Path in the *candidate* tree, or the baseline tree for a removal. */
|
|
18
|
+
readonly path: NodePath;
|
|
19
|
+
/** Which property, attribute, or ARIA state moved. Absent for whole-node deltas. */
|
|
20
|
+
readonly property?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Explicitly `| undefined`, unlike most optional fields in this codebase.
|
|
23
|
+
*
|
|
24
|
+
* A delta routinely has a value on one side only — an attribute that appeared,
|
|
25
|
+
* a role that was removed — and "absent on this side" is the meaning being
|
|
26
|
+
* recorded, not a field the author forgot. Forcing callers to omit the key
|
|
27
|
+
* instead would make every comparison site build its object conditionally to
|
|
28
|
+
* express something the type can say directly.
|
|
29
|
+
*/
|
|
30
|
+
readonly from?: string | undefined;
|
|
31
|
+
readonly to?: string | undefined;
|
|
32
|
+
/** Owner chain at the changed node, innermost first. */
|
|
33
|
+
readonly owners?: readonly OwnerFrame[];
|
|
34
|
+
/**
|
|
35
|
+
* The component whose JSX created this element.
|
|
36
|
+
*
|
|
37
|
+
* Distinct from `owners[0]`, which is the nearest *enclosing* component, and
|
|
38
|
+
* the two diverge exactly where structural attribution needs them to. When a
|
|
39
|
+
* list reorders, the nodes that moved are `Chip`s enclosed by a `Stack` — but
|
|
40
|
+
* neither decided the order. The component that wrote the JSX did, and this is
|
|
41
|
+
* the only field that names it.
|
|
42
|
+
*/
|
|
43
|
+
readonly createdBy?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Custom property the changed value resolved through.
|
|
46
|
+
*
|
|
47
|
+
* Present only when the token's own value moved. A node whose colour changed
|
|
48
|
+
* because `--color-primary` changed is collateral of one token edit; a node
|
|
49
|
+
* whose colour changed while every token held is a root in its own right.
|
|
50
|
+
* Distinguishing the two is the entire job of the docket.
|
|
51
|
+
*/
|
|
52
|
+
readonly token?: string;
|
|
53
|
+
/** Nearest ARIA landmark, so location reads as "right panel", not coordinates. */
|
|
54
|
+
readonly region?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Where this is, spoken outside-in: `main → region "Todos" → list item 2 of 3`.
|
|
57
|
+
*
|
|
58
|
+
* A path is an address and a rect is a coordinate; neither survives the next
|
|
59
|
+
* reflow or tells anyone where to look. See `locate.ts`.
|
|
60
|
+
*/
|
|
61
|
+
readonly where?: string;
|
|
62
|
+
readonly rectFrom?: Rect;
|
|
63
|
+
readonly rectTo?: Rect;
|
|
64
|
+
/**
|
|
65
|
+
* How far this change can reach: reflow, repaint, or compositing.
|
|
66
|
+
*
|
|
67
|
+
* Absent for structural deltas, where the question does not apply. See
|
|
68
|
+
* `impact.ts` — the useful consequence is that a paint-only change has no
|
|
69
|
+
* geometric collateral, which lets a profile with no layout engine rule out
|
|
70
|
+
* movement rather than merely failing to observe it.
|
|
71
|
+
*/
|
|
72
|
+
readonly impact?: PropertyImpact;
|
|
73
|
+
/**
|
|
74
|
+
* Set on a delta that is a *consequence* of another on the same node.
|
|
75
|
+
*
|
|
76
|
+
* A rect that moved because padding changed is not an independent finding —
|
|
77
|
+
* it is the padding change, observed a second way. Without this the docket
|
|
78
|
+
* reports one edit twice, once as `token` and once as `geometry`, and a
|
|
79
|
+
* reviewer has to work out that they are the same thing.
|
|
80
|
+
*/
|
|
81
|
+
readonly derivedFrom?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* What explains a group of deltas.
|
|
85
|
+
*
|
|
86
|
+
* The docket presents one entry per root (spec §6.2): *root cause, band, count
|
|
87
|
+
* of collateral, sample subjects, owner chain* — and approving it MUST be one
|
|
88
|
+
* action. "1 token change, 300 collateral, structure intact" is one review item.
|
|
89
|
+
*/
|
|
90
|
+
export interface Root {
|
|
91
|
+
/** Stable across builds, so an approval recorded against it keeps applying. */
|
|
92
|
+
readonly id: string;
|
|
93
|
+
readonly kind: RootKind;
|
|
94
|
+
readonly label: string;
|
|
95
|
+
readonly band: Band;
|
|
96
|
+
/**
|
|
97
|
+
* Whether this root can move things, or only repaint them.
|
|
98
|
+
*
|
|
99
|
+
* The first question a reviewer asks about a design-system change, and the one
|
|
100
|
+
* that decides whether the blast radius needs looking at.
|
|
101
|
+
*/
|
|
102
|
+
readonly impact: AggregateImpact;
|
|
103
|
+
/**
|
|
104
|
+
* The component responsible, when one can be named.
|
|
105
|
+
*
|
|
106
|
+
* Distinct from where the change was *observed*, and the two differ exactly on
|
|
107
|
+
* a `prop` root: `Panel → Button` means `Panel` passed something new and
|
|
108
|
+
* `Button` is where it shows. Naming `Button` sends a reviewer to a file
|
|
109
|
+
* nobody edited, which is the failure `prop-primary-variant/hero` exists to
|
|
110
|
+
* catch — and which `componentsOf` was committing, marking the innermost owner
|
|
111
|
+
* as `root` for every kind of root including this one.
|
|
112
|
+
*/
|
|
113
|
+
readonly cause?: string;
|
|
114
|
+
readonly deltas: readonly Delta[];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* A component implicated in a change set.
|
|
118
|
+
*
|
|
119
|
+
* `root` is the component the change originated in; `collateral` merely renders
|
|
120
|
+
* something the change reached. Separating them is the difference between "these
|
|
121
|
+
* eleven components changed" — which reads like eleven problems — and "`Button`
|
|
122
|
+
* changed, and ten components render it".
|
|
123
|
+
*/
|
|
124
|
+
export interface ChangedComponent {
|
|
125
|
+
readonly name: string;
|
|
126
|
+
readonly role: 'root' | 'collateral';
|
|
127
|
+
readonly deltaCount: number;
|
|
128
|
+
readonly bands: readonly Band[];
|
|
129
|
+
readonly impact: AggregateImpact;
|
|
130
|
+
/**
|
|
131
|
+
* Components that enclose this one where the change was observed.
|
|
132
|
+
*
|
|
133
|
+
* Answers "where does this show up?" — the propagation half of the spec's
|
|
134
|
+
* running sentence, *`Button` (variant prop change) → propagated to `NewHero`*.
|
|
135
|
+
*/
|
|
136
|
+
readonly renderedIn: readonly string[];
|
|
137
|
+
}
|
|
138
|
+
export type RootKind =
|
|
139
|
+
/** A design token's value moved. Every affected node is collateral. */
|
|
140
|
+
'token'
|
|
141
|
+
/** A component changed internally, with its incoming props unchanged. */
|
|
142
|
+
| 'component'
|
|
143
|
+
/** A component's incoming props changed; the root is upstream of it. */
|
|
144
|
+
| 'prop'
|
|
145
|
+
/** A render input that is not code: engine, fonts, viewport, ruleset. */
|
|
146
|
+
| 'environment'
|
|
147
|
+
/**
|
|
148
|
+
* No owner could be determined.
|
|
149
|
+
*
|
|
150
|
+
* Not a filler category. Provenance is supposed to make every change
|
|
151
|
+
* nameable, so an unattributed root means the chain broke — which is a defect
|
|
152
|
+
* in this tool, not a property of the change, and it is surfaced rather than
|
|
153
|
+
* quietly absorbed into a neighbouring root.
|
|
154
|
+
*/
|
|
155
|
+
| 'unattributed';
|
|
156
|
+
/**
|
|
157
|
+
* How a delta counts towards an aggregate impact.
|
|
158
|
+
*
|
|
159
|
+
* A structural delta has no `impact` because the question does not apply to it —
|
|
160
|
+
* a node that appeared did not reflow, it arrived. `'structural'` says that,
|
|
161
|
+
* rather than leaving every aggregation site to re-decide what a missing field
|
|
162
|
+
* means; there are three of them, and they must agree.
|
|
163
|
+
*/
|
|
164
|
+
export declare function impactTag(delta: Delta): PropertyImpact | 'structural';
|
|
165
|
+
//# sourceMappingURL=delta.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a delta counts towards an aggregate impact.
|
|
3
|
+
*
|
|
4
|
+
* A structural delta has no `impact` because the question does not apply to it —
|
|
5
|
+
* a node that appeared did not reflow, it arrived. `'structural'` says that,
|
|
6
|
+
* rather than leaving every aggregation site to re-decide what a missing field
|
|
7
|
+
* means; there are three of them, and they must agree.
|
|
8
|
+
*/
|
|
9
|
+
export function impactTag(delta) {
|
|
10
|
+
return delta.impact ?? 'structural';
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=delta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delta.js","sourceRoot":"","sources":["../../../src/compare/diff/delta.ts"],"names":[],"mappings":"AA8KA;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,KAAY;IACpC,OAAO,KAAK,CAAC,MAAM,IAAI,YAAY,CAAC;AACtC,CAAC","sourcesContent":["import type { Band, DeltaKind } from '../band.js';\nimport type { AggregateImpact, PropertyImpact } from '../impact.js';\nimport type { Rect } from '../../format/capture.js';\nimport type { NodePath } from '../../format/snapshot.js';\nimport type { OwnerFrame } from '../../format/provenance.js';\n\n/**\n * A single observed difference.\n *\n * Deltas are the atoms the docket is assembled from. Each one already knows its\n * band and its owner chain when it is created — Principle 5 says causality flows\n * forward, so a change must arrive carrying its name rather than having one\n * reconstructed for it later.\n */\nexport interface Delta {\n readonly kind: DeltaKind;\n readonly band: Band;\n\n /** Path in the *candidate* tree, or the baseline tree for a removal. */\n readonly path: NodePath;\n\n /** Which property, attribute, or ARIA state moved. Absent for whole-node deltas. */\n readonly property?: string;\n\n /**\n * Explicitly `| undefined`, unlike most optional fields in this codebase.\n *\n * A delta routinely has a value on one side only — an attribute that appeared,\n * a role that was removed — and \"absent on this side\" is the meaning being\n * recorded, not a field the author forgot. Forcing callers to omit the key\n * instead would make every comparison site build its object conditionally to\n * express something the type can say directly.\n */\n readonly from?: string | undefined;\n readonly to?: string | undefined;\n\n /** Owner chain at the changed node, innermost first. */\n readonly owners?: readonly OwnerFrame[];\n\n /**\n * The component whose JSX created this element.\n *\n * Distinct from `owners[0]`, which is the nearest *enclosing* component, and\n * the two diverge exactly where structural attribution needs them to. When a\n * list reorders, the nodes that moved are `Chip`s enclosed by a `Stack` — but\n * neither decided the order. The component that wrote the JSX did, and this is\n * the only field that names it.\n */\n readonly createdBy?: string;\n\n /**\n * Custom property the changed value resolved through.\n *\n * Present only when the token's own value moved. A node whose colour changed\n * because `--color-primary` changed is collateral of one token edit; a node\n * whose colour changed while every token held is a root in its own right.\n * Distinguishing the two is the entire job of the docket.\n */\n readonly token?: string;\n\n /** Nearest ARIA landmark, so location reads as \"right panel\", not coordinates. */\n readonly region?: string;\n\n /**\n * Where this is, spoken outside-in: `main → region \"Todos\" → list item 2 of 3`.\n *\n * A path is an address and a rect is a coordinate; neither survives the next\n * reflow or tells anyone where to look. See `locate.ts`.\n */\n readonly where?: string;\n\n readonly rectFrom?: Rect;\n readonly rectTo?: Rect;\n\n /**\n * How far this change can reach: reflow, repaint, or compositing.\n *\n * Absent for structural deltas, where the question does not apply. See\n * `impact.ts` — the useful consequence is that a paint-only change has no\n * geometric collateral, which lets a profile with no layout engine rule out\n * movement rather than merely failing to observe it.\n */\n readonly impact?: PropertyImpact;\n\n /**\n * Set on a delta that is a *consequence* of another on the same node.\n *\n * A rect that moved because padding changed is not an independent finding —\n * it is the padding change, observed a second way. Without this the docket\n * reports one edit twice, once as `token` and once as `geometry`, and a\n * reviewer has to work out that they are the same thing.\n */\n readonly derivedFrom?: string;\n}\n\n/**\n * What explains a group of deltas.\n *\n * The docket presents one entry per root (spec §6.2): *root cause, band, count\n * of collateral, sample subjects, owner chain* — and approving it MUST be one\n * action. \"1 token change, 300 collateral, structure intact\" is one review item.\n */\nexport interface Root {\n /** Stable across builds, so an approval recorded against it keeps applying. */\n readonly id: string;\n readonly kind: RootKind;\n readonly label: string;\n readonly band: Band;\n\n /**\n * Whether this root can move things, or only repaint them.\n *\n * The first question a reviewer asks about a design-system change, and the one\n * that decides whether the blast radius needs looking at.\n */\n readonly impact: AggregateImpact;\n\n /**\n * The component responsible, when one can be named.\n *\n * Distinct from where the change was *observed*, and the two differ exactly on\n * a `prop` root: `Panel → Button` means `Panel` passed something new and\n * `Button` is where it shows. Naming `Button` sends a reviewer to a file\n * nobody edited, which is the failure `prop-primary-variant/hero` exists to\n * catch — and which `componentsOf` was committing, marking the innermost owner\n * as `root` for every kind of root including this one.\n */\n readonly cause?: string;\n\n readonly deltas: readonly Delta[];\n}\n\n/**\n * A component implicated in a change set.\n *\n * `root` is the component the change originated in; `collateral` merely renders\n * something the change reached. Separating them is the difference between \"these\n * eleven components changed\" — which reads like eleven problems — and \"`Button`\n * changed, and ten components render it\".\n */\nexport interface ChangedComponent {\n readonly name: string;\n readonly role: 'root' | 'collateral';\n readonly deltaCount: number;\n readonly bands: readonly Band[];\n readonly impact: AggregateImpact;\n /**\n * Components that enclose this one where the change was observed.\n *\n * Answers \"where does this show up?\" — the propagation half of the spec's\n * running sentence, *`Button` (variant prop change) → propagated to `NewHero`*.\n */\n readonly renderedIn: readonly string[];\n}\n\nexport type RootKind =\n /** A design token's value moved. Every affected node is collateral. */\n | 'token'\n /** A component changed internally, with its incoming props unchanged. */\n | 'component'\n /** A component's incoming props changed; the root is upstream of it. */\n | 'prop'\n /** A render input that is not code: engine, fonts, viewport, ruleset. */\n | 'environment'\n /**\n * No owner could be determined.\n *\n * Not a filler category. Provenance is supposed to make every change\n * nameable, so an unattributed root means the chain broke — which is a defect\n * in this tool, not a property of the change, and it is surfaced rather than\n * quietly absorbed into a neighbouring root.\n */\n | 'unattributed';\n\n/**\n * How a delta counts towards an aggregate impact.\n *\n * A structural delta has no `impact` because the question does not apply to it —\n * a node that appeared did not reflow, it arrived. `'structural'` says that,\n * rather than leaving every aggregation site to re-decide what a missing field\n * means; there are three of them, and they must agree.\n */\nexport function impactTag(delta: Delta): PropertyImpact | 'structural' {\n return delta.impact ?? 'structural';\n}\n"]}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Band } from '../band.js';
|
|
2
|
+
import { type AggregateImpact } from '../impact.js';
|
|
3
|
+
import { type EnvironmentDelta } from '../../format/environment.js';
|
|
4
|
+
import type { SemanticSnapshot } from '../../format/snapshot.js';
|
|
5
|
+
import { type ChangedComponent, type Delta, type Root } from './delta.js';
|
|
6
|
+
export type { ChangedComponent, Delta, Root, RootKind } from './delta.js';
|
|
7
|
+
export { matchTrees } from './match.js';
|
|
8
|
+
export type { Matching } from './match.js';
|
|
9
|
+
export interface SemanticDiff {
|
|
10
|
+
readonly subjectId: string;
|
|
11
|
+
/** `true` when both render hashes agree — nothing below need be consulted. */
|
|
12
|
+
readonly identical: boolean;
|
|
13
|
+
readonly environmentDeltas: readonly EnvironmentDelta[];
|
|
14
|
+
readonly deltas: readonly Delta[];
|
|
15
|
+
/** One entry per explanation. This is what a docket renders and approves. */
|
|
16
|
+
readonly roots: readonly Root[];
|
|
17
|
+
/**
|
|
18
|
+
* Components implicated, separated into causes and collateral.
|
|
19
|
+
*
|
|
20
|
+
* "Eleven components changed" reads like eleven problems. "`Button` changed,
|
|
21
|
+
* and ten components render it" reads like one, which is what it is.
|
|
22
|
+
*/
|
|
23
|
+
readonly components: readonly ChangedComponent[];
|
|
24
|
+
/**
|
|
25
|
+
* Whether anything in this change set can move a box.
|
|
26
|
+
*
|
|
27
|
+
* `paint` or `composite` means nothing reflowed and no geometric collateral is
|
|
28
|
+
* possible — a conclusion available without a layout engine, which is how a
|
|
29
|
+
* profile that cannot measure still rules movement out instead of merely
|
|
30
|
+
* failing to observe it.
|
|
31
|
+
*/
|
|
32
|
+
readonly impact: AggregateImpact;
|
|
33
|
+
/**
|
|
34
|
+
* Bands this profile could not observe.
|
|
35
|
+
*
|
|
36
|
+
* Reported so that "no geometry deltas" is never mistaken for "geometry is
|
|
37
|
+
* fine" under a profile with no layout engine (ADR-0002).
|
|
38
|
+
*/
|
|
39
|
+
readonly unobserved: readonly Band[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Compare two snapshots of one subject.
|
|
43
|
+
*
|
|
44
|
+
* @throws {Error} when the snapshots come from different subjects or different
|
|
45
|
+
* observation profiles. Neither is a large diff — they are a category error, and
|
|
46
|
+
* returning deltas for them would let a JSDOM run appear to satisfy a Chromium
|
|
47
|
+
* baseline while blind to every geometry change in it.
|
|
48
|
+
*/
|
|
49
|
+
export declare function diffSnapshots(baseline: SemanticSnapshot, candidate: SemanticSnapshot): SemanticDiff;
|
|
50
|
+
/** Everything a {@link SemanticDiff} is except the subject it is about. */
|
|
51
|
+
export type TreeComparison = Omit<SemanticDiff, 'subjectId'>;
|
|
52
|
+
/**
|
|
53
|
+
* The comparison itself, after whatever refusals the caller owes.
|
|
54
|
+
*
|
|
55
|
+
* Split out because two callers owe different refusals over the same arithmetic.
|
|
56
|
+
* `diffSnapshots` produces a **verdict** and must refuse two subjects, because a
|
|
57
|
+
* verdict that crossed them would let one subject's baseline stand in for
|
|
58
|
+
* another's. `deriveVariation` produces an **explanation** of a difference
|
|
59
|
+
* somebody declared on purpose — a dark story against its light parent — where
|
|
60
|
+
* two subject ids are the entire point and a refusal would be the bug.
|
|
61
|
+
*
|
|
62
|
+
* What is not negotiable either way is the observability rule: a band is
|
|
63
|
+
* `unobserved` unless **both** sides could decide it, so a difference that only
|
|
64
|
+
* one profile could have seen is never reported as no difference (ADR-0002).
|
|
65
|
+
* Under `diffSnapshots` the two profiles are already identical, so the union
|
|
66
|
+
* costs it nothing and changes none of its answers.
|
|
67
|
+
*/
|
|
68
|
+
export declare function compareTrees(baseline: SemanticSnapshot, candidate: SemanticSnapshot): TreeComparison;
|
|
69
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { BANDS } from '../band.js';
|
|
2
|
+
import { aggregateImpact } from '../impact.js';
|
|
3
|
+
import { diffEnvironments } from '../../format/environment.js';
|
|
4
|
+
import { decidesBand, observableBands } from '../observability.js';
|
|
5
|
+
import { impactTag } from './delta.js';
|
|
6
|
+
import { matchTrees } from './match.js';
|
|
7
|
+
import { compareNodes, wholeNode } from './compare-nodes.js';
|
|
8
|
+
import { attribute } from './attribution.js';
|
|
9
|
+
import { componentsOf } from './components.js';
|
|
10
|
+
import { locate } from '../../attribute/locate.js';
|
|
11
|
+
export { matchTrees } from './match.js';
|
|
12
|
+
/**
|
|
13
|
+
* Compare two snapshots of one subject.
|
|
14
|
+
*
|
|
15
|
+
* @throws {Error} when the snapshots come from different subjects or different
|
|
16
|
+
* observation profiles. Neither is a large diff — they are a category error, and
|
|
17
|
+
* returning deltas for them would let a JSDOM run appear to satisfy a Chromium
|
|
18
|
+
* baseline while blind to every geometry change in it.
|
|
19
|
+
*/
|
|
20
|
+
export function diffSnapshots(baseline, candidate) {
|
|
21
|
+
if (baseline.subject.id !== candidate.subject.id) {
|
|
22
|
+
throw new Error(`refusing to diff different subjects: ${baseline.subject.id} vs ${candidate.subject.id}`);
|
|
23
|
+
}
|
|
24
|
+
if (baseline.profile.id !== candidate.profile.id) {
|
|
25
|
+
throw new Error(`refusing to diff across observation profiles: ${baseline.profile.id} vs ${candidate.profile.id}`);
|
|
26
|
+
}
|
|
27
|
+
return { subjectId: candidate.subject.id, ...compareTrees(baseline, candidate) };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The comparison itself, after whatever refusals the caller owes.
|
|
31
|
+
*
|
|
32
|
+
* Split out because two callers owe different refusals over the same arithmetic.
|
|
33
|
+
* `diffSnapshots` produces a **verdict** and must refuse two subjects, because a
|
|
34
|
+
* verdict that crossed them would let one subject's baseline stand in for
|
|
35
|
+
* another's. `deriveVariation` produces an **explanation** of a difference
|
|
36
|
+
* somebody declared on purpose — a dark story against its light parent — where
|
|
37
|
+
* two subject ids are the entire point and a refusal would be the bug.
|
|
38
|
+
*
|
|
39
|
+
* What is not negotiable either way is the observability rule: a band is
|
|
40
|
+
* `unobserved` unless **both** sides could decide it, so a difference that only
|
|
41
|
+
* one profile could have seen is never reported as no difference (ADR-0002).
|
|
42
|
+
* Under `diffSnapshots` the two profiles are already identical, so the union
|
|
43
|
+
* costs it nothing and changes none of its answers.
|
|
44
|
+
*/
|
|
45
|
+
export function compareTrees(baseline, candidate) {
|
|
46
|
+
const here = observableBands(candidate.profile);
|
|
47
|
+
const there = observableBands(baseline.profile);
|
|
48
|
+
const unobserved = BANDS.filter((band) => !decidesBand(band, here[band]) || !decidesBand(band, there[band]));
|
|
49
|
+
const environmentDeltas = diffEnvironments(baseline.environment.inputs, candidate.environment.inputs);
|
|
50
|
+
if (baseline.renderHash === candidate.renderHash) {
|
|
51
|
+
return {
|
|
52
|
+
identical: true,
|
|
53
|
+
environmentDeltas,
|
|
54
|
+
deltas: [],
|
|
55
|
+
roots: [],
|
|
56
|
+
components: [],
|
|
57
|
+
impact: 'paint',
|
|
58
|
+
unobserved,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const matching = matchTrees(baseline.root, candidate.root);
|
|
62
|
+
const deltas = [];
|
|
63
|
+
for (const node of matching.added) {
|
|
64
|
+
deltas.push(wholeNode('node-added', node));
|
|
65
|
+
}
|
|
66
|
+
for (const node of matching.removed) {
|
|
67
|
+
deltas.push(wholeNode('node-removed', node));
|
|
68
|
+
}
|
|
69
|
+
for (const [before, after] of matching.pairs) {
|
|
70
|
+
if (matching.moved.has(before)) {
|
|
71
|
+
deltas.push(wholeNode('node-moved', after));
|
|
72
|
+
}
|
|
73
|
+
compareNodes(before, after, deltas, candidate.profile.layout && baseline.profile.layout);
|
|
74
|
+
}
|
|
75
|
+
// Orientation is attached after the deltas exist, in one pass over the
|
|
76
|
+
// candidate tree. Doing it during comparison would locate against whichever
|
|
77
|
+
// tree that comparison happened to be holding, and a removal must be located
|
|
78
|
+
// in the tree it was removed *from* — see below.
|
|
79
|
+
const located = deltas.map((delta) => {
|
|
80
|
+
const tree = delta.kind === 'node-removed' ? baseline.root : candidate.root;
|
|
81
|
+
const where = locate(tree, delta.path);
|
|
82
|
+
return where.where === ''
|
|
83
|
+
? delta
|
|
84
|
+
: {
|
|
85
|
+
...delta,
|
|
86
|
+
where: where.where,
|
|
87
|
+
...(where.region !== undefined ? { region: where.region } : {}),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
const roots = attribute(located, matching, environmentDeltas);
|
|
91
|
+
return {
|
|
92
|
+
identical: false,
|
|
93
|
+
environmentDeltas,
|
|
94
|
+
deltas: located,
|
|
95
|
+
roots,
|
|
96
|
+
components: componentsOf(located, roots),
|
|
97
|
+
impact: aggregateImpact(located.map(impactTag)),
|
|
98
|
+
unobserved,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/compare/diff/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { BANDS, type Band } from '../band.js';\nimport { aggregateImpact, type AggregateImpact } from '../impact.js';\nimport { diffEnvironments, type EnvironmentDelta } from '../../format/environment.js';\nimport type { SemanticSnapshot } from '../../format/snapshot.js';\nimport { decidesBand, observableBands } from '../observability.js';\nimport { impactTag, type ChangedComponent, type Delta, type Root } from './delta.js';\nimport { matchTrees } from './match.js';\nimport { compareNodes, wholeNode } from './compare-nodes.js';\nimport { attribute } from './attribution.js';\nimport { componentsOf } from './components.js';\nimport { locate } from '../../attribute/locate.js';\n\nexport type { ChangedComponent, Delta, Root, RootKind } from './delta.js';\nexport { matchTrees } from './match.js';\nexport type { Matching } from './match.js';\n\nexport interface SemanticDiff {\n readonly subjectId: string;\n /** `true` when both render hashes agree — nothing below need be consulted. */\n readonly identical: boolean;\n\n readonly environmentDeltas: readonly EnvironmentDelta[];\n readonly deltas: readonly Delta[];\n\n /** One entry per explanation. This is what a docket renders and approves. */\n readonly roots: readonly Root[];\n\n /**\n * Components implicated, separated into causes and collateral.\n *\n * \"Eleven components changed\" reads like eleven problems. \"`Button` changed,\n * and ten components render it\" reads like one, which is what it is.\n */\n readonly components: readonly ChangedComponent[];\n\n /**\n * Whether anything in this change set can move a box.\n *\n * `paint` or `composite` means nothing reflowed and no geometric collateral is\n * possible — a conclusion available without a layout engine, which is how a\n * profile that cannot measure still rules movement out instead of merely\n * failing to observe it.\n */\n readonly impact: AggregateImpact;\n\n /**\n * Bands this profile could not observe.\n *\n * Reported so that \"no geometry deltas\" is never mistaken for \"geometry is\n * fine\" under a profile with no layout engine (ADR-0002).\n */\n readonly unobserved: readonly Band[];\n}\n\n/**\n * Compare two snapshots of one subject.\n *\n * @throws {Error} when the snapshots come from different subjects or different\n * observation profiles. Neither is a large diff — they are a category error, and\n * returning deltas for them would let a JSDOM run appear to satisfy a Chromium\n * baseline while blind to every geometry change in it.\n */\nexport function diffSnapshots(\n baseline: SemanticSnapshot,\n candidate: SemanticSnapshot,\n): SemanticDiff {\n if (baseline.subject.id !== candidate.subject.id) {\n throw new Error(\n `refusing to diff different subjects: ${baseline.subject.id} vs ${candidate.subject.id}`,\n );\n }\n\n if (baseline.profile.id !== candidate.profile.id) {\n throw new Error(\n `refusing to diff across observation profiles: ${baseline.profile.id} vs ${candidate.profile.id}`,\n );\n }\n\n return { subjectId: candidate.subject.id, ...compareTrees(baseline, candidate) };\n}\n\n/** Everything a {@link SemanticDiff} is except the subject it is about. */\nexport type TreeComparison = Omit<SemanticDiff, 'subjectId'>;\n\n/**\n * The comparison itself, after whatever refusals the caller owes.\n *\n * Split out because two callers owe different refusals over the same arithmetic.\n * `diffSnapshots` produces a **verdict** and must refuse two subjects, because a\n * verdict that crossed them would let one subject's baseline stand in for\n * another's. `deriveVariation` produces an **explanation** of a difference\n * somebody declared on purpose — a dark story against its light parent — where\n * two subject ids are the entire point and a refusal would be the bug.\n *\n * What is not negotiable either way is the observability rule: a band is\n * `unobserved` unless **both** sides could decide it, so a difference that only\n * one profile could have seen is never reported as no difference (ADR-0002).\n * Under `diffSnapshots` the two profiles are already identical, so the union\n * costs it nothing and changes none of its answers.\n */\nexport function compareTrees(\n baseline: SemanticSnapshot,\n candidate: SemanticSnapshot,\n): TreeComparison {\n const here = observableBands(candidate.profile);\n const there = observableBands(baseline.profile);\n const unobserved: Band[] = BANDS.filter(\n (band) => !decidesBand(band, here[band]) || !decidesBand(band, there[band]),\n );\n\n const environmentDeltas = diffEnvironments(\n baseline.environment.inputs,\n candidate.environment.inputs,\n );\n\n if (baseline.renderHash === candidate.renderHash) {\n return {\n identical: true,\n environmentDeltas,\n deltas: [],\n roots: [],\n components: [],\n impact: 'paint',\n unobserved,\n };\n }\n\n const matching = matchTrees(baseline.root, candidate.root);\n const deltas: Delta[] = [];\n\n for (const node of matching.added) {\n deltas.push(wholeNode('node-added', node));\n }\n for (const node of matching.removed) {\n deltas.push(wholeNode('node-removed', node));\n }\n\n for (const [before, after] of matching.pairs) {\n if (matching.moved.has(before)) {\n deltas.push(wholeNode('node-moved', after));\n }\n compareNodes(before, after, deltas, candidate.profile.layout && baseline.profile.layout);\n }\n\n // Orientation is attached after the deltas exist, in one pass over the\n // candidate tree. Doing it during comparison would locate against whichever\n // tree that comparison happened to be holding, and a removal must be located\n // in the tree it was removed *from* — see below.\n const located = deltas.map((delta) => {\n const tree = delta.kind === 'node-removed' ? baseline.root : candidate.root;\n const where = locate(tree, delta.path);\n\n return where.where === ''\n ? delta\n : {\n ...delta,\n where: where.where,\n ...(where.region !== undefined ? { region: where.region } : {}),\n };\n });\n\n const roots = attribute(located, matching, environmentDeltas);\n\n return {\n identical: false,\n environmentDeltas,\n deltas: located,\n roots,\n components: componentsOf(located, roots),\n impact: aggregateImpact(located.map(impactTag)),\n unobserved,\n };\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SemanticNode } from '../../format/snapshot.js';
|
|
2
|
+
/**
|
|
3
|
+
* Node correspondence.
|
|
4
|
+
*
|
|
5
|
+
* Paths are positional, so inserting one sibling renumbers everything after it.
|
|
6
|
+
* Diffing by path alone would report a single insertion as "every following node
|
|
7
|
+
* changed" — the classic snapshot-test failure that trains people to stop
|
|
8
|
+
* reading diffs. Matching therefore happens by identity first, content second,
|
|
9
|
+
* and position last.
|
|
10
|
+
*
|
|
11
|
+
* The middle pass is the one that took three attempts to find. Identity keys
|
|
12
|
+
* collapse whenever a node has no alias, no accessible name and no provenance —
|
|
13
|
+
* five bare `<li>`s bucket together as `tag:li` — and pairing inside that bucket
|
|
14
|
+
* by position turns a rotation into "every item's text changed". The corpus case
|
|
15
|
+
* `reorder/list` was written to catch exactly that and could not: its assertions
|
|
16
|
+
* were the verdict and the root count, and both are correct while the report is
|
|
17
|
+
* useless. Scoring the *band* is what exposed it — five `text-changed` deltas
|
|
18
|
+
* band as `content` where a rotation bands as `geometry`.
|
|
19
|
+
*/
|
|
20
|
+
export interface Matching {
|
|
21
|
+
/** Baseline node → candidate node. */
|
|
22
|
+
readonly pairs: ReadonlyMap<SemanticNode, SemanticNode>;
|
|
23
|
+
readonly added: readonly SemanticNode[];
|
|
24
|
+
readonly removed: readonly SemanticNode[];
|
|
25
|
+
/**
|
|
26
|
+
* Pairs whose position among their siblings changed *relative to each other*.
|
|
27
|
+
*
|
|
28
|
+
* Not "whose index differs". Prepending one item shifts every index after it
|
|
29
|
+
* without reordering anything, and reporting three moves for one insertion is
|
|
30
|
+
* the same fatigue that positional diffing produces. The moved set is the
|
|
31
|
+
* complement of the longest run that stayed in order, which is the smallest
|
|
32
|
+
* set of nodes whose movement explains the new sequence.
|
|
33
|
+
*/
|
|
34
|
+
readonly moved: ReadonlySet<SemanticNode>;
|
|
35
|
+
}
|
|
36
|
+
export declare function matchTrees(baseline: SemanticNode, candidate: SemanticNode): Matching;
|
|
37
|
+
//# sourceMappingURL=match.d.ts.map
|