@variance-authority/core 0.14.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +878 -0
- package/README.md +38 -17
- package/dist/attribute/component-hash.d.ts +14 -5
- package/dist/attribute/component-hash.js +14 -5
- package/dist/attribute/component-hash.js.map +1 -1
- package/dist/attribute/composition.d.ts +16 -0
- package/dist/attribute/composition.js +4 -6
- package/dist/attribute/composition.js.map +1 -1
- package/dist/attribute/index.d.ts +3 -1
- package/dist/attribute/index.js +2 -1
- package/dist/attribute/index.js.map +1 -1
- package/dist/attribute/movement.d.ts +1 -1
- package/dist/attribute/movement.js.map +1 -1
- package/dist/attribute/region.d.ts +47 -4
- package/dist/attribute/region.js +91 -22
- package/dist/attribute/region.js.map +1 -1
- package/dist/attribute/source.d.ts +10 -24
- package/dist/attribute/source.js +4 -38
- package/dist/attribute/source.js.map +1 -1
- package/dist/attribute/subject-pieces.d.ts +37 -0
- package/dist/attribute/subject-pieces.js +122 -0
- package/dist/attribute/subject-pieces.js.map +1 -0
- package/dist/compare/band.d.ts +8 -2
- package/dist/compare/band.js +7 -2
- package/dist/compare/band.js.map +1 -1
- package/dist/compare/diff/attribution.js +6 -4
- package/dist/compare/diff/attribution.js.map +1 -1
- package/dist/compare/diff/compare-nodes.js +9 -1
- package/dist/compare/diff/compare-nodes.js.map +1 -1
- package/dist/compare/diff/match.d.ts +2 -1
- package/dist/compare/diff/match.js +52 -0
- package/dist/compare/diff/match.js.map +1 -1
- package/dist/compare/parting.js +6 -4
- package/dist/compare/parting.js.map +1 -1
- package/dist/format/capture.d.ts +25 -0
- package/dist/format/capture.js +6 -0
- package/dist/format/capture.js.map +1 -1
- package/dist/format/document.d.ts +28 -5
- package/dist/format/document.js +8 -2
- package/dist/format/document.js.map +1 -1
- package/dist/format/drive.d.ts +191 -0
- package/dist/format/drive.js +343 -0
- package/dist/format/drive.js.map +1 -0
- package/dist/format/environment.d.ts +20 -2
- package/dist/format/environment.js +7 -0
- package/dist/format/environment.js.map +1 -1
- package/dist/format/index.d.ts +13 -3
- package/dist/format/index.js +8 -2
- package/dist/format/index.js.map +1 -1
- package/dist/format/intervention.d.ts +79 -0
- package/dist/format/intervention.js.map +1 -1
- package/dist/format/origin.d.ts +32 -0
- package/dist/format/origin.js +54 -0
- package/dist/format/origin.js.map +1 -0
- package/dist/format/packed.d.ts +32 -0
- package/dist/format/packed.js +23 -0
- package/dist/format/packed.js.map +1 -0
- package/dist/format/shape.d.ts +31 -0
- package/dist/format/shape.js +269 -0
- package/dist/format/shape.js.map +1 -0
- package/dist/format/snapshot.d.ts +2 -0
- package/dist/format/snapshot.js.map +1 -1
- package/dist/format/stabilize.d.ts +35 -6
- package/dist/format/stabilize.js +58 -103
- package/dist/format/stabilize.js.map +1 -1
- package/dist/format/steps.d.ts +44 -0
- package/dist/format/steps.js +67 -0
- package/dist/format/steps.js.map +1 -0
- package/dist/format/waits.d.ts +12 -0
- package/dist/format/waits.js +156 -0
- package/dist/format/waits.js.map +1 -0
- package/dist/format/width.d.ts +35 -0
- package/dist/format/width.js +38 -0
- package/dist/format/width.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/judge/docket.js +1 -0
- package/dist/judge/docket.js.map +1 -1
- package/dist/judge/index.d.ts +1 -0
- package/dist/judge/index.js +1 -0
- package/dist/judge/index.js.map +1 -1
- package/dist/judge/restamp.d.ts +20 -0
- package/dist/judge/restamp.js +20 -0
- package/dist/judge/restamp.js.map +1 -0
- package/dist/nearest.d.ts +46 -0
- package/dist/nearest.js +102 -0
- package/dist/nearest.js.map +1 -0
- package/dist/relate/before.d.ts +19 -24
- package/dist/relate/before.js +37 -39
- package/dist/relate/before.js.map +1 -1
- package/dist/relate/beyond.d.ts +59 -0
- package/dist/relate/beyond.js +98 -0
- package/dist/relate/beyond.js.map +1 -0
- package/dist/relate/carried.d.ts +122 -0
- package/dist/relate/carried.js +279 -0
- package/dist/relate/carried.js.map +1 -0
- package/dist/relate/graph.d.ts +14 -12
- package/dist/relate/graph.js +12 -11
- package/dist/relate/graph.js.map +1 -1
- package/dist/relate/index.d.ts +3 -1
- package/dist/relate/index.js +3 -1
- package/dist/relate/index.js.map +1 -1
- package/dist/relate/merkle.js +2 -11
- package/dist/relate/merkle.js.map +1 -1
- package/dist/relate/narrow.d.ts +2 -2
- package/dist/relate/narrow.js +26 -73
- package/dist/relate/narrow.js.map +1 -1
- package/dist/relate/reach.d.ts +6 -6
- package/dist/relate/reach.js +10 -53
- package/dist/relate/reach.js.map +1 -1
- package/dist/relate/records.d.ts +6 -5
- package/dist/relate/records.js +3 -2
- package/dist/relate/records.js.map +1 -1
- package/dist/rules/index.d.ts +3 -3
- package/dist/rules/index.js +3 -3
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/normalize/cascade.d.ts +12 -0
- package/dist/rules/normalize/cascade.js +34 -16
- package/dist/rules/normalize/cascade.js.map +1 -1
- package/dist/rules/normalize/fixture.d.ts +3 -0
- package/dist/rules/normalize/fixture.js +3 -1
- package/dist/rules/normalize/fixture.js.map +1 -1
- package/dist/rules/normalize/index.d.ts +2 -2
- package/dist/rules/normalize/index.js +9 -2
- package/dist/rules/normalize/index.js.map +1 -1
- package/dist/rules/normalize/value.d.ts +13 -2
- package/dist/rules/normalize/value.js +42 -8
- package/dist/rules/normalize/value.js.map +1 -1
- package/dist/rules/normalize/wrapper.js +5 -0
- package/dist/rules/normalize/wrapper.js.map +1 -1
- package/dist/rules/ruleset.d.ts +24 -4
- package/dist/rules/ruleset.js +24 -4
- package/dist/rules/ruleset.js.map +1 -1
- package/dist/segment/index.d.ts +20 -4
- package/dist/segment/index.js +28 -7
- package/dist/segment/index.js.map +1 -1
- package/dist/shard/index.d.ts +11 -0
- package/dist/shard/index.js +11 -0
- package/dist/shard/index.js.map +1 -0
- package/dist/shard/placement.d.ts +119 -0
- package/dist/shard/placement.js +156 -0
- package/dist/shard/placement.js.map +1 -0
- package/dist/share/index.d.ts +1 -1
- package/dist/share/index.js +1 -1
- package/dist/share/index.js.map +1 -1
- package/package.json +8 -1
package/dist/attribute/source.js
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
* configuring a build, and it answers with where a component is *declared* —
|
|
22
22
|
* coarser than a call site, and enough to open the right file.
|
|
23
23
|
*
|
|
24
|
-
* The index is plain data. `core` performs no I/O (ADR-0006)
|
|
25
|
-
* a caller's job
|
|
24
|
+
* The index is plain data. `core` performs no I/O (ADR-0006) and has no parser,
|
|
25
|
+
* so building one is a caller's job — `indexDeclarations` in
|
|
26
|
+
* [`@variance-authority/sense`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/sense/src/declarations.ts) reads it off
|
|
27
|
+
* a module's parse — and resolving against one is here.
|
|
26
28
|
*/
|
|
27
29
|
export function resolveSource(name, index) {
|
|
28
30
|
const refs = index[name];
|
|
@@ -46,42 +48,6 @@ export function formatSource(resolution) {
|
|
|
46
48
|
const others = resolution.refs.slice(1).map((ref) => `${ref.file}:${ref.line}`);
|
|
47
49
|
return `${primary} (ambiguous — also ${others.join(', ')})`;
|
|
48
50
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Extract component declarations from one file's source.
|
|
51
|
-
*
|
|
52
|
-
* A regex scan, and the limits are worth stating rather than discovering. It
|
|
53
|
-
* finds exported and local declarations in the three shapes React components are
|
|
54
|
-
* written in, and it will miss a component produced by a factory, assigned
|
|
55
|
-
* dynamically, or re-exported under another name. It can also match a function
|
|
56
|
-
* that merely looks like a component — capitalised, declared at top level — and
|
|
57
|
-
* name a non-component in a report.
|
|
58
|
-
*
|
|
59
|
-
* Both failures are survivable in a way a wrong file path would not be: a miss
|
|
60
|
-
* degrades the report to the component name, which is what it said before, and a
|
|
61
|
-
* false positive can only appear if attribution already named that identifier.
|
|
62
|
-
* Parsing properly is the right answer eventually; it is not worth a parser
|
|
63
|
-
* dependency to find out whether the link is useful.
|
|
64
|
-
*/
|
|
65
|
-
export function indexSource(file, contents) {
|
|
66
|
-
const found = {};
|
|
67
|
-
const lines = contents.split('\n');
|
|
68
|
-
const patterns = [
|
|
69
|
-
{ re: /^\s*(?:export\s+)?(?:default\s+)?function\s+([A-Z][A-Za-z0-9_]*)/, via: 'function' },
|
|
70
|
-
{ re: /^\s*(?:export\s+)?(?:const|let)\s+([A-Z][A-Za-z0-9_]*)\s*[:=]/, via: 'const' },
|
|
71
|
-
{ re: /^\s*(?:export\s+)?(?:default\s+)?class\s+([A-Z][A-Za-z0-9_]*)/, via: 'class' },
|
|
72
|
-
];
|
|
73
|
-
for (const [index, line] of lines.entries()) {
|
|
74
|
-
for (const { re, via } of patterns) {
|
|
75
|
-
const match = re.exec(line);
|
|
76
|
-
if (!match)
|
|
77
|
-
continue;
|
|
78
|
-
const name = match[1];
|
|
79
|
-
(found[name] ??= []).push({ file, line: index + 1, via });
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return found;
|
|
84
|
-
}
|
|
85
51
|
/** Merge per-file indexes. A name declared in several files keeps every ref. */
|
|
86
52
|
export function mergeSourceIndexes(indexes) {
|
|
87
53
|
const merged = {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/attribute/source.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/attribute/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAoCH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,KAAkB;IAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;AACpD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,UAAsB;IACjD,MAAM,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC;IAChC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,IAAI,CAAC;IAEhD,MAAM,OAAO,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,SAAS;QAAE,OAAO,OAAO,CAAC;IAE1C,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAChF,OAAO,GAAG,OAAO,sBAAsB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;AAC9D,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,kBAAkB,CAAC,OAA+B;IAChE,MAAM,MAAM,GAAgC,EAAE,CAAC;IAE/C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjD,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAiB,EAAE,OAAoB;IACxE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC;AACjC,CAAC","sourcesContent":["/**\n * Connecting a change to a file.\n *\n * A report that says `Button` is one step short of useful. An agent asked to fix\n * it still has to find `Button`, and a reviewer still has to guess whether the\n * `Button` in question is the design-system one or the local one in checkout.\n * The last hop — **which file** — is what turns a finding into an edit.\n *\n * There are two mechanisms, and this file is the second one.\n *\n * The first is exact. Spec §6.1 assumes the build tells us: every JSX transform\n * already computes each element's file, line and column, and\n * `@variance-authority/jsx-source` is the `jsxImportSource` setting that keeps\n * that location as far as the fiber — React 19 drops it otherwise, on every path.\n * When it is on, a finding names the element's own line and this index is not\n * consulted.\n *\n * The second is this one, and it is what a repository that has changed nothing\n * gets. Attribution names *components*, so a **component → file** index answers\n * the question attribution asks. It is built by reading source rather than by\n * configuring a build, and it answers with where a component is *declared* —\n * coarser than a call site, and enough to open the right file.\n *\n * The index is plain data. `core` performs no I/O (ADR-0006) and has no parser,\n * so building one is a caller's job — `indexDeclarations` in\n * [`@variance-authority/sense`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/sense/src/declarations.ts) reads it off\n * a module's parse — and resolving against one is here.\n */\n\nexport interface SourceRef {\n /** Repository-relative, so a report is portable between machines and CI. */\n readonly file: string;\n readonly line: number;\n /**\n * How the declaration was recognised. Carried so a bad match is debuggable.\n *\n * The first three say which kind of statement declared the name; a `let`\n * binding is shown as `const`. `engine` is the one that is not read from\n * source at all: the browser was asked where the function it rendered was\n * compiled from, and that position was mapped back to the file. It names the\n * component that ran, whatever it was called.\n */\n readonly via: 'function' | 'const' | 'class' | 'engine';\n}\n\n/**\n * Component name → where it is declared.\n *\n * A name may map to several files. That is not an error to be resolved by\n * picking one: two components genuinely can share a name, and silently choosing\n * the first would send an agent to edit the wrong file with full confidence.\n * Ambiguity is reported (ADR-0003's rule for overlapping projections, applied to\n * source).\n */\nexport type SourceIndex = Readonly<Record<string, readonly SourceRef[]>>;\n\nexport interface Resolution {\n readonly name: string;\n readonly refs: readonly SourceRef[];\n /** `true` when more than one file declares this name. */\n readonly ambiguous: boolean;\n}\n\nexport function resolveSource(name: string, index: SourceIndex): Resolution | null {\n const refs = index[name];\n if (refs === undefined || refs.length === 0) return null;\n\n return { name, refs, ambiguous: refs.length > 1 };\n}\n\n/**\n * `file:line`, or a disambiguating list when a name is declared more than once.\n *\n * Formatted as `path:line` because that is the form an editor, a terminal, and\n * every agent harness already know how to open.\n */\nexport function formatSource(resolution: Resolution): string {\n const [first] = resolution.refs;\n if (first === undefined) return resolution.name;\n\n const primary = `${first.file}:${first.line}`;\n if (!resolution.ambiguous) return primary;\n\n const others = resolution.refs.slice(1).map((ref) => `${ref.file}:${ref.line}`);\n return `${primary} (ambiguous — also ${others.join(', ')})`;\n}\n\n/** Merge per-file indexes. A name declared in several files keeps every ref. */\nexport function mergeSourceIndexes(indexes: readonly SourceIndex[]): SourceIndex {\n const merged: Record<string, SourceRef[]> = {};\n\n for (const index of indexes) {\n for (const [name, refs] of Object.entries(index)) {\n (merged[name] ??= []).push(...refs);\n }\n }\n\n return merged;\n}\n\n/**\n * One index laid over another: a name the overlay holds replaces the base's refs.\n *\n * Not a merge. A scan finds every file that declares a `Button`, and reports the\n * pair as ambiguous because the text cannot say which one a page rendered. The\n * engine can, and when it has answered for a name the scan's guesses for that\n * name are no longer the question. A name the overlay never saw keeps what the\n * base had, so a component the page did not mount still has a file.\n */\nexport function overlaySourceIndex(base: SourceIndex, overlay: SourceIndex): SourceIndex {\n return { ...base, ...overlay };\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type SubjectComposition } from './composition.js';
|
|
2
|
+
/** Another subject beside the one read. */
|
|
3
|
+
export interface SubjectShare {
|
|
4
|
+
readonly subject: string;
|
|
5
|
+
/** Its footprint: distinct renderings outside structure. */
|
|
6
|
+
readonly footprint: number;
|
|
7
|
+
/** Renderings of the subject read that it holds too. */
|
|
8
|
+
readonly shared: number;
|
|
9
|
+
}
|
|
10
|
+
/** Renderings of one component that no piece holds. */
|
|
11
|
+
export interface Residue {
|
|
12
|
+
readonly component: string;
|
|
13
|
+
readonly renderings: number;
|
|
14
|
+
/** The pieces that mount the component and render it otherwise. Absent on a component no piece mounts. */
|
|
15
|
+
readonly pieces?: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
export interface SubjectPieces {
|
|
18
|
+
/** Distinct renderings it holds outside structure. */
|
|
19
|
+
readonly footprint: number;
|
|
20
|
+
/** Structural components it mounts. */
|
|
21
|
+
readonly structure: number;
|
|
22
|
+
/** Subjects whose footprint is exactly its own, in plan order. */
|
|
23
|
+
readonly alike: readonly string[];
|
|
24
|
+
/** Smaller subjects inside it, most shared first. */
|
|
25
|
+
readonly pieces: readonly SubjectShare[];
|
|
26
|
+
/** Larger subjects holding it, smallest first. */
|
|
27
|
+
readonly wholes: readonly SubjectShare[];
|
|
28
|
+
/** Renderings of its footprint some piece holds. */
|
|
29
|
+
readonly explained: number;
|
|
30
|
+
/** Residue in components no piece mounts, by component name. */
|
|
31
|
+
readonly own: readonly Residue[];
|
|
32
|
+
/** Residue in components a piece mounts and renders otherwise, by component name. */
|
|
33
|
+
readonly inContext: readonly Residue[];
|
|
34
|
+
}
|
|
35
|
+
/** Every subject's pieces, keyed by subject. A pure fold over the suite, so it reads the same however the run was scheduled. */
|
|
36
|
+
export declare function piecesOf(subjects: readonly SubjectComposition[]): ReadonlyMap<string, SubjectPieces>;
|
|
37
|
+
//# sourceMappingURL=subject-pieces.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { structuralComponents } from './composition.js';
|
|
2
|
+
import { attributed } from './instances.js';
|
|
3
|
+
import { codeUnitOrder } from '../segment/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Each subject read as the narrower subjects inside it.
|
|
6
|
+
*
|
|
7
|
+
* The same algebra the recording answers for a test, over renderings instead
|
|
8
|
+
* of regions. A subject's footprint is the distinct renderings it holds — a
|
|
9
|
+
* component and the digest of what it rendered — less structure, the
|
|
10
|
+
* components more than half the suite mounts. A piece is a smaller subject with
|
|
11
|
+
* at least nine tenths of its footprint inside this one: the chip story inside
|
|
12
|
+
* the footer story inside the page. A whole is a larger subject holding at
|
|
13
|
+
* least nine tenths of this one. A subject with exactly the same footprint is
|
|
14
|
+
* alike, and is neither.
|
|
15
|
+
*
|
|
16
|
+
* What no piece renders is the residue, counted rendering by rendering and
|
|
17
|
+
* never as a difference of totals. It splits in two. A component no piece
|
|
18
|
+
* mounts is the subject's own: only it shows that component, so only it can
|
|
19
|
+
* catch a change there. A component a piece mounts, rendered here in a way the
|
|
20
|
+
* piece never renders it, is the component in context — the narrow subject
|
|
21
|
+
* does not watch this rendering, and a change to it is the page's to catch.
|
|
22
|
+
*
|
|
23
|
+
* It decides nothing. A subject whose pieces explain all of it is not therefore
|
|
24
|
+
* redundant: the bytes a page holds are the bytes its pieces hold, and the
|
|
25
|
+
* arrangement between them is not a rendering of any one component.
|
|
26
|
+
*/
|
|
27
|
+
/** A piece holds at least this share of its own footprint inside the subject; a whole holds at least this share of the subject's. */
|
|
28
|
+
const HOLDS = 0.9;
|
|
29
|
+
/** Every subject's pieces, keyed by subject. A pure fold over the suite, so it reads the same however the run was scheduled. */
|
|
30
|
+
export function piecesOf(subjects) {
|
|
31
|
+
const structural = structuralComponents(subjects);
|
|
32
|
+
const footprints = new Map();
|
|
33
|
+
const mounted = new Map();
|
|
34
|
+
const structure = new Map();
|
|
35
|
+
const holders = new Map();
|
|
36
|
+
for (const { subject, instances } of subjects) {
|
|
37
|
+
const held = new Set();
|
|
38
|
+
const components = new Set();
|
|
39
|
+
const frame = new Set();
|
|
40
|
+
for (const instance of instances) {
|
|
41
|
+
if (!attributed(instance))
|
|
42
|
+
continue;
|
|
43
|
+
if (structural.has(instance.component)) {
|
|
44
|
+
frame.add(instance.component);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
held.add(keyOf(instance.component, instance.rendering));
|
|
48
|
+
components.add(instance.component);
|
|
49
|
+
}
|
|
50
|
+
footprints.set(subject, held);
|
|
51
|
+
mounted.set(subject, components);
|
|
52
|
+
structure.set(subject, frame.size);
|
|
53
|
+
for (const key of held) {
|
|
54
|
+
const list = holders.get(key);
|
|
55
|
+
if (list === undefined)
|
|
56
|
+
holders.set(key, [subject]);
|
|
57
|
+
else
|
|
58
|
+
list.push(subject);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const order = new Map(subjects.map(({ subject }, at) => [subject, at]));
|
|
62
|
+
const answers = new Map();
|
|
63
|
+
for (const { subject } of subjects) {
|
|
64
|
+
const mine = footprints.get(subject) ?? new Set();
|
|
65
|
+
const size = mine.size;
|
|
66
|
+
const shared = new Map();
|
|
67
|
+
for (const key of mine) {
|
|
68
|
+
for (const other of holders.get(key) ?? []) {
|
|
69
|
+
if (other !== subject)
|
|
70
|
+
shared.set(other, (shared.get(other) ?? 0) + 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const pieces = [];
|
|
74
|
+
const wholes = [];
|
|
75
|
+
const alike = [];
|
|
76
|
+
for (const [other, with_] of shared) {
|
|
77
|
+
const theirs = footprints.get(other)?.size ?? 0;
|
|
78
|
+
const share = { subject: other, footprint: theirs, shared: with_ };
|
|
79
|
+
if (theirs < size && with_ >= HOLDS * theirs)
|
|
80
|
+
pieces.push(share);
|
|
81
|
+
else if (theirs > size && with_ >= HOLDS * size)
|
|
82
|
+
wholes.push(share);
|
|
83
|
+
else if (theirs === size && with_ === size)
|
|
84
|
+
alike.push(other);
|
|
85
|
+
}
|
|
86
|
+
const planned = (a, b) => (order.get(a.subject) ?? 0) - (order.get(b.subject) ?? 0);
|
|
87
|
+
pieces.sort((a, b) => b.shared - a.shared || b.footprint - a.footprint || planned(a, b));
|
|
88
|
+
wholes.sort((a, b) => a.footprint - b.footprint || planned(a, b));
|
|
89
|
+
alike.sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
|
|
90
|
+
const kept = new Set(pieces.map((piece) => piece.subject));
|
|
91
|
+
let explained = 0;
|
|
92
|
+
const left = new Map();
|
|
93
|
+
for (const key of mine) {
|
|
94
|
+
if ((holders.get(key) ?? []).some((other) => kept.has(other)))
|
|
95
|
+
explained += 1;
|
|
96
|
+
else {
|
|
97
|
+
const component = componentOf(key);
|
|
98
|
+
left.set(component, (left.get(component) ?? 0) + 1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const own = [];
|
|
102
|
+
const inContext = [];
|
|
103
|
+
for (const component of [...left.keys()].sort(codeUnitOrder)) {
|
|
104
|
+
const renderings = left.get(component) ?? 0;
|
|
105
|
+
const by = pieces.map((piece) => piece.subject).filter((piece) => mounted.get(piece)?.has(component) === true);
|
|
106
|
+
if (by.length === 0)
|
|
107
|
+
own.push({ component, renderings });
|
|
108
|
+
else
|
|
109
|
+
inContext.push({ component, renderings, pieces: by });
|
|
110
|
+
}
|
|
111
|
+
answers.set(subject, { footprint: size, structure: structure.get(subject) ?? 0, alike, pieces, wholes, explained, own, inContext });
|
|
112
|
+
}
|
|
113
|
+
return answers;
|
|
114
|
+
}
|
|
115
|
+
/** A component and a rendering, joined by a character no component name holds. */
|
|
116
|
+
function keyOf(component, rendering) {
|
|
117
|
+
return `${component}\u0000${rendering}`;
|
|
118
|
+
}
|
|
119
|
+
function componentOf(key) {
|
|
120
|
+
return key.slice(0, key.indexOf('\u0000'));
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=subject-pieces.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subject-pieces.js","sourceRoot":"","sources":["../../src/attribute/subject-pieces.ts"],"names":[],"mappings":"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","sourcesContent":["import { structuralComponents, type SubjectComposition } from './composition.js';\nimport { attributed } from './instances.js';\nimport { codeUnitOrder } from '../segment/index.js';\n\n/**\n * Each subject read as the narrower subjects inside it.\n *\n * The same algebra the recording answers for a test, over renderings instead\n * of regions. A subject's footprint is the distinct renderings it holds — a\n * component and the digest of what it rendered — less structure, the\n * components more than half the suite mounts. A piece is a smaller subject with\n * at least nine tenths of its footprint inside this one: the chip story inside\n * the footer story inside the page. A whole is a larger subject holding at\n * least nine tenths of this one. A subject with exactly the same footprint is\n * alike, and is neither.\n *\n * What no piece renders is the residue, counted rendering by rendering and\n * never as a difference of totals. It splits in two. A component no piece\n * mounts is the subject's own: only it shows that component, so only it can\n * catch a change there. A component a piece mounts, rendered here in a way the\n * piece never renders it, is the component in context — the narrow subject\n * does not watch this rendering, and a change to it is the page's to catch.\n *\n * It decides nothing. A subject whose pieces explain all of it is not therefore\n * redundant: the bytes a page holds are the bytes its pieces hold, and the\n * arrangement between them is not a rendering of any one component.\n */\n\n/** A piece holds at least this share of its own footprint inside the subject; a whole holds at least this share of the subject's. */\nconst HOLDS = 0.9;\n\n/** Another subject beside the one read. */\nexport interface SubjectShare {\n readonly subject: string;\n /** Its footprint: distinct renderings outside structure. */\n readonly footprint: number;\n /** Renderings of the subject read that it holds too. */\n readonly shared: number;\n}\n\n/** Renderings of one component that no piece holds. */\nexport interface Residue {\n readonly component: string;\n readonly renderings: number;\n /** The pieces that mount the component and render it otherwise. Absent on a component no piece mounts. */\n readonly pieces?: readonly string[];\n}\n\nexport interface SubjectPieces {\n /** Distinct renderings it holds outside structure. */\n readonly footprint: number;\n /** Structural components it mounts. */\n readonly structure: number;\n /** Subjects whose footprint is exactly its own, in plan order. */\n readonly alike: readonly string[];\n /** Smaller subjects inside it, most shared first. */\n readonly pieces: readonly SubjectShare[];\n /** Larger subjects holding it, smallest first. */\n readonly wholes: readonly SubjectShare[];\n /** Renderings of its footprint some piece holds. */\n readonly explained: number;\n /** Residue in components no piece mounts, by component name. */\n readonly own: readonly Residue[];\n /** Residue in components a piece mounts and renders otherwise, by component name. */\n readonly inContext: readonly Residue[];\n}\n\n/** Every subject's pieces, keyed by subject. A pure fold over the suite, so it reads the same however the run was scheduled. */\nexport function piecesOf(subjects: readonly SubjectComposition[]): ReadonlyMap<string, SubjectPieces> {\n const structural = structuralComponents(subjects);\n const footprints = new Map<string, ReadonlySet<string>>();\n const mounted = new Map<string, ReadonlySet<string>>();\n const structure = new Map<string, number>();\n const holders = new Map<string, string[]>();\n for (const { subject, instances } of subjects) {\n const held = new Set<string>();\n const components = new Set<string>();\n const frame = new Set<string>();\n for (const instance of instances) {\n if (!attributed(instance)) continue;\n if (structural.has(instance.component)) {\n frame.add(instance.component);\n continue;\n }\n held.add(keyOf(instance.component, instance.rendering));\n components.add(instance.component);\n }\n footprints.set(subject, held);\n mounted.set(subject, components);\n structure.set(subject, frame.size);\n for (const key of held) {\n const list = holders.get(key);\n if (list === undefined) holders.set(key, [subject]);\n else list.push(subject);\n }\n }\n\n const order = new Map(subjects.map(({ subject }, at) => [subject, at]));\n const answers = new Map<string, SubjectPieces>();\n for (const { subject } of subjects) {\n const mine = footprints.get(subject) ?? new Set<string>();\n const size = mine.size;\n const shared = new Map<string, number>();\n for (const key of mine) {\n for (const other of holders.get(key) ?? []) {\n if (other !== subject) shared.set(other, (shared.get(other) ?? 0) + 1);\n }\n }\n const pieces: SubjectShare[] = [];\n const wholes: SubjectShare[] = [];\n const alike: string[] = [];\n for (const [other, with_] of shared) {\n const theirs = footprints.get(other)?.size ?? 0;\n const share = { subject: other, footprint: theirs, shared: with_ };\n if (theirs < size && with_ >= HOLDS * theirs) pieces.push(share);\n else if (theirs > size && with_ >= HOLDS * size) wholes.push(share);\n else if (theirs === size && with_ === size) alike.push(other);\n }\n const planned = (a: SubjectShare, b: SubjectShare) => (order.get(a.subject) ?? 0) - (order.get(b.subject) ?? 0);\n pieces.sort((a, b) => b.shared - a.shared || b.footprint - a.footprint || planned(a, b));\n wholes.sort((a, b) => a.footprint - b.footprint || planned(a, b));\n alike.sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));\n\n const kept = new Set(pieces.map((piece) => piece.subject));\n let explained = 0;\n const left = new Map<string, number>();\n for (const key of mine) {\n if ((holders.get(key) ?? []).some((other) => kept.has(other))) explained += 1;\n else {\n const component = componentOf(key);\n left.set(component, (left.get(component) ?? 0) + 1);\n }\n }\n const own: Residue[] = [];\n const inContext: Residue[] = [];\n for (const component of [...left.keys()].sort(codeUnitOrder)) {\n const renderings = left.get(component) ?? 0;\n const by = pieces.map((piece) => piece.subject).filter((piece) => mounted.get(piece)?.has(component) === true);\n if (by.length === 0) own.push({ component, renderings });\n else inContext.push({ component, renderings, pieces: by });\n }\n answers.set(subject, { footprint: size, structure: structure.get(subject) ?? 0, alike, pieces, wholes, explained, own, inContext });\n }\n return answers;\n}\n\n/** A component and a rendering, joined by a character no component name holds. */\nfunction keyOf(component: string, rendering: string): string {\n return `${component}\\u0000${rendering}`;\n}\n\nfunction componentOf(key: string): string {\n return key.slice(0, key.indexOf('\\u0000'));\n}\n"]}
|
package/dist/compare/band.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ export declare const BANDS: readonly Band[];
|
|
|
59
59
|
*/
|
|
60
60
|
export declare function loudestBand(bands: Iterable<Band>): Band | null;
|
|
61
61
|
/** What kind of thing changed, before policy has an opinion about it. */
|
|
62
|
-
export type DeltaKind = 'node-added' | 'node-removed' | 'node-moved' | 'role-changed' | 'name-changed' | 'description-changed' | 'state-changed' | 'text-changed' | 'attribute-changed' | 'rect-changed' | 'style-changed' | 'token-changed' | 'raster-residue';
|
|
62
|
+
export type DeltaKind = 'node-added' | 'node-removed' | 'node-moved' | 'tag-changed' | 'role-changed' | 'name-changed' | 'description-changed' | 'state-changed' | 'text-changed' | 'attribute-changed' | 'rect-changed' | 'style-changed' | 'token-changed' | 'raster-residue';
|
|
63
63
|
/**
|
|
64
64
|
* The band a delta falls in, before policy.
|
|
65
65
|
*
|
|
@@ -78,8 +78,14 @@ export type DeltaKind = 'node-added' | 'node-removed' | 'node-moved' | 'role-cha
|
|
|
78
78
|
* there is no rect evidence at all, and the style change stands alone — which is
|
|
79
79
|
* why JSDOM can decide the token band but only part of geometry.
|
|
80
80
|
*
|
|
81
|
+
* A style delta on a pair across two tags, `retagged`, is `geometry`, with the
|
|
82
|
+
* `tag-changed` it follows, because the structure a `token` delta says held did
|
|
83
|
+
* not.
|
|
84
|
+
*
|
|
81
85
|
* Policy may promote or demote from here per project, subject, region, or
|
|
82
86
|
* component (spec §5). This function is the default, not the answer.
|
|
83
87
|
*/
|
|
84
|
-
export declare function bandOf(kind: DeltaKind
|
|
88
|
+
export declare function bandOf(kind: DeltaKind, { retagged }?: {
|
|
89
|
+
readonly retagged?: boolean;
|
|
90
|
+
}): Band;
|
|
85
91
|
//# sourceMappingURL=band.d.ts.map
|
package/dist/compare/band.js
CHANGED
|
@@ -42,10 +42,14 @@ export function loudestBand(bands) {
|
|
|
42
42
|
* there is no rect evidence at all, and the style change stands alone — which is
|
|
43
43
|
* why JSDOM can decide the token band but only part of geometry.
|
|
44
44
|
*
|
|
45
|
+
* A style delta on a pair across two tags, `retagged`, is `geometry`, with the
|
|
46
|
+
* `tag-changed` it follows, because the structure a `token` delta says held did
|
|
47
|
+
* not.
|
|
48
|
+
*
|
|
45
49
|
* Policy may promote or demote from here per project, subject, region, or
|
|
46
50
|
* component (spec §5). This function is the default, not the answer.
|
|
47
51
|
*/
|
|
48
|
-
export function bandOf(kind) {
|
|
52
|
+
export function bandOf(kind, { retagged = false } = {}) {
|
|
49
53
|
switch (kind) {
|
|
50
54
|
case 'role-changed':
|
|
51
55
|
case 'name-changed':
|
|
@@ -57,12 +61,13 @@ export function bandOf(kind) {
|
|
|
57
61
|
case 'node-added':
|
|
58
62
|
case 'node-removed':
|
|
59
63
|
case 'node-moved':
|
|
64
|
+
case 'tag-changed':
|
|
60
65
|
case 'attribute-changed':
|
|
61
66
|
case 'rect-changed':
|
|
62
67
|
return 'geometry';
|
|
63
68
|
case 'style-changed':
|
|
64
69
|
case 'token-changed':
|
|
65
|
-
return 'token';
|
|
70
|
+
return retagged ? 'geometry' : 'token';
|
|
66
71
|
case 'raster-residue':
|
|
67
72
|
return 'texture';
|
|
68
73
|
}
|
package/dist/compare/band.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"band.js","sourceRoot":"","sources":["../../src/compare/band.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAwCH,kFAAkF;AAClF,MAAM,CAAC,MAAM,KAAK,GAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;AAE1F;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAAqB;IAC/C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC/B,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;AACzD,CAAC;
|
|
1
|
+
{"version":3,"file":"band.js","sourceRoot":"","sources":["../../src/compare/band.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAwCH,kFAAkF;AAClF,MAAM,CAAC,MAAM,KAAK,GAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;AAE1F;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAAqB;IAC/C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC/B,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;AACzD,CAAC;AAmBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,MAAM,CAAC,IAAe,EAAE,EAAE,QAAQ,GAAG,KAAK,EAAE,GAAoC,EAAE;IAChG,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,cAAc,CAAC;QACpB,KAAK,cAAc,CAAC;QACpB,KAAK,qBAAqB,CAAC;QAC3B,KAAK,eAAe;YAClB,OAAO,MAAM,CAAC;QAChB,KAAK,cAAc;YACjB,OAAO,SAAS,CAAC;QACnB,KAAK,YAAY,CAAC;QAClB,KAAK,cAAc,CAAC;QACpB,KAAK,YAAY,CAAC;QAClB,KAAK,aAAa,CAAC;QACnB,KAAK,mBAAmB,CAAC;QACzB,KAAK,cAAc;YACjB,OAAO,UAAU,CAAC;QACpB,KAAK,eAAe,CAAC;QACrB,KAAK,eAAe;YAClB,OAAO,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;QACzC,KAAK,gBAAgB;YACnB,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC","sourcesContent":["/**\n * Frequency bands (spec §5).\n *\n * Every semantic delta lands in exactly one band, and the band decides how loudly\n * it is reported. The insight the bands encode: change frequency and change\n * importance are inversely correlated. An accessible name almost never moves and\n * is a defect when it does; anti-aliasing moves constantly and never matters.\n *\n * The list is ordered, loudest first, and `loudestBand` is the only thing allowed\n * to collapse a set of them. Three call sites used to hard-code their own\n * `if (bands.includes('geometry'))` ladder, which meant adding a band silently\n * demoted it below everything in every one of them.\n */\n\nexport type Band =\n /**\n * Rarest, and the one a picture cannot hold. A role, an accessible name, or an\n * ARIA state changed — what a screen reader announces moved, whether or not\n * anything was repainted.\n *\n * Split out of `geometry` deliberately. Both are low-frequency, but a box that\n * moved and a control that lost its name are different findings for different\n * readers, and folding the second into the first means the evidence this\n * project collects and no image-comparison tool has is reported under a label\n * that sounds like layout. A project can now write `blocking: ['a11y']` and\n * have an accessibility regression stop a merge on its own terms.\n */\n | 'a11y'\n /** Low frequency. Boxes appear, vanish, move, or resize; content reflows. */\n | 'geometry'\n /** Mid frequency. Style *values* moved while structure held. */\n | 'token'\n /**\n * Text moved and nothing else did.\n *\n * The highest-frequency band that is still a real change: copy edits, counts,\n * dates, translations. Quieter than `geometry` because a reviewer who is told\n * a string changed does not also need to be told a box moved — the reflow is\n * the consequence, and `geometry` outranks it exactly when the tree moved for\n * some other reason too.\n *\n * It is separated from `geometry` because it is the band a message catalogue\n * joins against. A locale run compares the same subject's `content` across two\n * languages, which is a question no baseline image can be asked.\n */\n | 'content'\n /**\n * High frequency. Sub-semantic rendering variance — anti-aliasing, sub-pixel\n * shifts, compression artifacts. Auto-passes; counted, never reviewed.\n */\n | 'texture';\n\n/** Loudest first. `loudestBand` reads this order; nothing else may assume one. */\nexport const BANDS: readonly Band[] = ['a11y', 'geometry', 'token', 'content', 'texture'];\n\n/**\n * The loudest band present, or `null` for an empty set.\n *\n * `null` rather than a default, because \"nothing changed\" and \"something changed\n * at the quietest band\" are different states and a caller that wants to conflate\n * them should have to say so.\n */\nexport function loudestBand(bands: Iterable<Band>): Band | null {\n const present = new Set(bands);\n return BANDS.find((band) => present.has(band)) ?? null;\n}\n\n/** What kind of thing changed, before policy has an opinion about it. */\nexport type DeltaKind =\n | 'node-added'\n | 'node-removed'\n | 'node-moved'\n | 'tag-changed'\n | 'role-changed'\n | 'name-changed'\n | 'description-changed'\n | 'state-changed'\n | 'text-changed'\n | 'attribute-changed'\n | 'rect-changed'\n | 'style-changed'\n | 'token-changed'\n | 'raster-residue';\n\n/**\n * The band a delta falls in, before policy.\n *\n * `attribute-changed` is `geometry` rather than `a11y` for a reason that is\n * easy to get backwards: **no `aria-*` attribute ever reaches it.**\n * `ATTRIBUTE_ALLOWLIST` drops them all, because each one resolves into a field\n * of its own — `role`, `name`, `description`, `state` — and those are the four\n * kinds banded `a11y`. An `aria-*` delta arriving as `attribute-changed` would\n * mean normalization had stopped resolving it, which is a defect rather than a\n * banding question.\n *\n * Note that `rect-changed` is `geometry` while `style-changed` is `token`, even\n * though a padding change produces both. That is intended: the two are reported\n * at different bands from different evidence, and the attributor folds the rect\n * movement into the style change as collateral. Under a profile without layout\n * there is no rect evidence at all, and the style change stands alone — which is\n * why JSDOM can decide the token band but only part of geometry.\n *\n * A style delta on a pair across two tags, `retagged`, is `geometry`, with the\n * `tag-changed` it follows, because the structure a `token` delta says held did\n * not.\n *\n * Policy may promote or demote from here per project, subject, region, or\n * component (spec §5). This function is the default, not the answer.\n */\nexport function bandOf(kind: DeltaKind, { retagged = false }: { readonly retagged?: boolean } = {}): Band {\n switch (kind) {\n case 'role-changed':\n case 'name-changed':\n case 'description-changed':\n case 'state-changed':\n return 'a11y';\n case 'text-changed':\n return 'content';\n case 'node-added':\n case 'node-removed':\n case 'node-moved':\n case 'tag-changed':\n case 'attribute-changed':\n case 'rect-changed':\n return 'geometry';\n case 'style-changed':\n case 'token-changed':\n return retagged ? 'geometry' : 'token';\n case 'raster-residue':\n return 'texture';\n }\n}\n"]}
|
|
@@ -66,10 +66,12 @@ export function attribute(deltas, matching, environmentDeltas) {
|
|
|
66
66
|
// change even though every node that moved is a `Chip`.
|
|
67
67
|
//
|
|
68
68
|
// Appearing and disappearing are deliberately *not* treated this way. A
|
|
69
|
-
// component that swaps its own output
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
69
|
+
// component that swaps its own output produces nodes whose creator is that
|
|
70
|
+
// component itself, and crediting whoever placed it would blame its parent
|
|
71
|
+
// for a change it did not make. One swap — `Toggle` rendering a `<div>`
|
|
72
|
+
// where it used to render an `<input>` — is a matched pair and arrives
|
|
73
|
+
// here as `tag-changed` (see `match.ts`); two swaps by one author stay an
|
|
74
|
+
// added and a removed node each, and the same rule names `Toggle` for both.
|
|
73
75
|
//
|
|
74
76
|
// Known cost: when a component element is added or removed wholesale, this
|
|
75
77
|
// names the component that appeared rather than the one that decided to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attribution.js","sourceRoot":"","sources":["../../../src/compare/diff/attribution.ts"],"names":[],"mappings":"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","sourcesContent":["import { loudestBand, type Band } from '../band.js';\nimport { aggregateImpact } from '../impact.js';\nimport type { EnvironmentDelta } from '../../format/environment.js';\nimport type { OwnerFrame } from '../../format/provenance.js';\nimport { impactTag, type Delta, type Root, type RootKind } from './delta.js';\nimport type { Matching } from './match.js';\n\n/**\n * Grouping deltas into explanations — the docket's entire value proposition.\n *\n * Separated from `index.ts` because it is the half of diffing that has nothing\n * to do with observation: it takes deltas that already exist and decides which\n * *one* sentence covers them. The precedence rules, the metric-delta folding and\n * the boundary walk are all in service of the same claim — report the largest\n * explanation once rather than the smallest one many times — and they only make\n * sense read together.\n */\n\n/**\n * Group deltas into explanations (spec §6.2).\n *\n * Precedence is fixed and deliberate: environment, then token, then props, then\n * component. Each level explains more subjects than the one below it, and the\n * docket's value is entirely in reporting the *largest* explanation once rather\n * than the smallest one many times.\n */\nexport function attribute(\n deltas: readonly Delta[],\n matching: Matching,\n environmentDeltas: readonly EnvironmentDelta[],\n): readonly Root[] {\n const groups = new Map<string, { kind: RootKind; label: string; cause?: string; deltas: Delta[] }>();\n\n const add = (id: string, kind: RootKind, label: string, delta: Delta, cause?: string): void => {\n const existing = groups.get(id);\n if (existing) existing.deltas.push(delta);\n else groups.set(id, { kind, label, deltas: [delta], ...(cause !== undefined ? { cause } : {}) });\n };\n\n // A changed environment explains everything under it, so it is collapsed to a\n // single root — the one-action re-baselining case of spec §7.3.\n if (environmentDeltas.length > 0) {\n for (const environmentDelta of environmentDeltas) {\n groups.set(`env:${environmentDelta.field}`, {\n kind: 'environment',\n label: `${environmentDelta.field} ${environmentDelta.from} → ${environmentDelta.to}`,\n deltas: [],\n });\n }\n }\n\n const propsBefore = ownerDigests(matching, 'before');\n const propsAfter = ownerDigests(matching, 'after');\n\n const classify = (delta: Delta): Group | null => {\n if (delta.token) return { id: `token:${delta.token}`, kind: 'token', label: delta.token };\n\n const owners = delta.owners;\n if (owners === undefined || owners.length === 0) return null;\n\n const boundary = changedBoundary(owners, propsBefore, propsAfter);\n\n if (boundary === null) {\n // Every incoming props digest held, so the change originated inside a\n // component rather than arriving from outside.\n //\n // *Which* component depends on what kind of change it is. A style change\n // belongs to the component the styled node sits in. A node appearing,\n // vanishing, or moving belongs to whoever *decided it is there* — the\n // component whose JSX created the element, not the component the element\n // is. When a filter list reorders, the nodes that moved are `Chip`s\n // enclosed by a `Stack`, and naming either reports the thing that was\n // rearranged instead of the code that rearranged it.\n const owner = owners[0]!;\n\n // A node that *moved* is the only case where the responsible component is\n // not the one the node belongs to. Ordering is decided by whoever wrote the\n // JSX that placed the element, so a reordered filter list is `TodoFooter`'s\n // change even though every node that moved is a `Chip`.\n //\n // Appearing and disappearing are deliberately *not* treated this way. A\n // component that swaps its own output — `Toggle` rendering a `<div>` where\n // it used to render an `<input>` — produces an added and a removed node\n // whose creator is `Toggle` itself, and crediting whoever placed `<Toggle>`\n // would blame `TodoItem` for a change it did not make.\n //\n // Known cost: when a component element is added or removed wholesale, this\n // names the component that appeared rather than the one that decided to\n // render it. Distinguishing those needs to know whether the component still\n // exists on the other side, which is a question about the change set rather\n // than about the delta.\n const name = (delta.kind === 'node-moved' ? owner.createdBy : undefined) ?? owner.name;\n\n return { id: `component:${name}`, kind: 'component', label: name, cause: name };\n }\n\n // Props moved at a boundary, so the change arrived from outside. The root is\n // the provider — the component that passes props across that boundary.\n const changed = owners[boundary]!;\n const provider = owners[boundary + 1];\n return {\n id: `prop:${provider?.name ?? '?'}>${changed.name}`,\n kind: 'prop',\n label: provider ? `${provider.name} → ${changed.name}` : changed.name,\n // The *provider*, not the component the delta landed in. This is the whole\n // point of a `prop` root: the edit is upstream, and the report has to send\n // a reviewer there.\n //\n // Set **only when a provider exists**. With none, the changed boundary is\n // the outermost frame in the chain: the props arrived from outside the\n // subject entirely, and no component inside it is responsible. Naming the\n // boundary there would send a reviewer to a component whose source is also\n // unchanged, which is the same failure one level out. The fallback in\n // `componentsOf` — the innermost owner, where the change landed — is what\n // both corpora ask for in that case, and the entry's own label still says\n // whose props moved.\n ...(provider !== undefined ? { cause: provider.name } : {}),\n };\n };\n\n // Metric deltas are held back until every cause has a group, then attached to\n // the nearest one. Without this a profile with layout reports a root per\n // component whose box happened to resize — the change is not lost, but \"one\n // root plus counted collateral\" becomes \"one root per affected component\",\n // which is the report spec §6.2 exists to prevent.\n const causes = deltas.filter((delta) => !isMetric(delta)).map((delta) => ({ delta, group: classify(delta) }));\n\n for (const { delta, group } of causes) {\n if (group === null) add('unattributed', 'unattributed', 'no owner chain', delta);\n else add(group.id, group.kind, group.label, delta, group.cause);\n }\n\n for (const delta of deltas) {\n if (!isMetric(delta)) continue;\n\n const target = nearestCause(delta, causes);\n\n if (target !== undefined) {\n add(target.id, target.kind, target.label, delta, target.cause);\n continue;\n }\n\n // No cause anywhere: the box moved and nothing explains it. That is a real\n // finding — most often a change outside the subject reaching in — and it\n // keeps its own root rather than being attached to an unrelated one.\n const own = classify(delta);\n if (own === null) add('unattributed', 'unattributed', 'no owner chain', delta);\n else add(own.id, own.kind, own.label, delta, own.cause);\n }\n\n return [...groups.entries()].map(([id, group]) => ({\n id,\n kind: group.kind,\n label: group.label,\n ...(group.cause !== undefined ? { cause: group.cause } : {}),\n band: dominantBand(group.deltas),\n impact: aggregateImpact(group.deltas.map(impactTag)),\n deltas: group.deltas,\n }));\n}\n\ninterface Group {\n readonly id: string;\n readonly kind: RootKind;\n readonly label: string;\n /** The component responsible. See `Root.cause`. */\n readonly cause?: string;\n}\n\n/**\n * Computed properties that are functions of the used box rather than of any\n * declaration.\n *\n * A layout engine reports these for every element whether or not a stylesheet\n * mentioned them, so they move whenever anything inside or above the node\n * changes size. Treating them as causes is what turned one token edit into five\n * docket entries under `chromium` — `band.ts` already says the attributor must\n * fold rect movement into the style change as collateral, and these are the\n * `style-changed` half of the same evidence.\n *\n * Note the cost of the simplification: a genuine `width: 100px → 200px` edit is\n * folded too, whenever the same node carries another delta to fold into. That\n * loses no delta and moves no verdict — the change is still reported, under a\n * root that is a strictly better description of the cause.\n */\nconst USED_VALUE_PROPERTIES: ReadonlySet<string> = new Set([\n 'width', 'height', 'inline-size', 'block-size',\n 'transform-origin', 'perspective-origin',\n]);\n\n/** Evidence that a box moved, as opposed to evidence of why. */\nfunction isMetric(delta: Delta): boolean {\n if (delta.kind === 'rect-changed') return true;\n return delta.kind === 'style-changed' && USED_VALUE_PROPERTIES.has(delta.property ?? '');\n}\n\n/** Paths are `/`-joined child indices, so containment is a prefix test. */\nfunction isDescendant(candidate: string, ancestor: string): boolean {\n return candidate.startsWith(`${ancestor}/`);\n}\n\n/**\n * The cause a metric delta belongs to: same node first, then the nearest change\n * *inside* it, then the nearest change above it.\n *\n * Inside before above, because a box that grew did so because of its contents\n * far more often than because of its container — and when a container really is\n * the cause, its own metric delta folds upward on the same rule, so the two\n * meet at the same root either way.\n */\nfunction nearestCause(\n delta: Delta,\n causes: readonly { readonly delta: Delta; readonly group: Group | null }[],\n): Group | undefined {\n const grouped = causes.filter((cause) => cause.group !== null);\n\n const here = grouped.find((cause) => cause.delta.path === delta.path);\n if (here) return here.group ?? undefined;\n\n const inside = grouped\n .filter((cause) => isDescendant(cause.delta.path, delta.path))\n .sort((a, b) => a.delta.path.length - b.delta.path.length)[0];\n if (inside) return inside.group ?? undefined;\n\n const above = grouped\n .filter((cause) => isDescendant(delta.path, cause.delta.path))\n .sort((a, b) => b.delta.path.length - a.delta.path.length)[0];\n\n return above?.group ?? undefined;\n}\n\n/**\n * The outermost owner whose incoming props changed, or `null` if none did.\n *\n * Outermost rather than innermost: if `CheckoutPage` passes a new prop to\n * `Header`, which passes it to `Button`, all three boundaries moved, and the\n * useful root is the one furthest up — reporting `Button` would name the\n * messenger.\n */\nfunction changedBoundary(\n owners: readonly OwnerFrame[],\n before: ReadonlyMap<string, string>,\n after: ReadonlyMap<string, string>,\n): number | null {\n let outermost: number | null = null;\n\n for (let index = 0; index < owners.length; index += 1) {\n const owner = owners[index]!;\n const digestBefore = before.get(owner.name);\n // An owner absent from the baseline is new structure, not a prop change;\n // the structural delta covers it and inventing a prop root would double-count.\n if (digestBefore === undefined) continue;\n if (digestBefore !== after.get(owner.name)) outermost = index;\n }\n\n return outermost;\n}\n\nfunction ownerDigests(\n matching: Matching,\n side: 'before' | 'after',\n): ReadonlyMap<string, string> {\n const digests = new Map<string, string>();\n\n for (const [before, after] of matching.pairs) {\n const node = side === 'before' ? before : after;\n for (const owner of node.provenance?.owners ?? []) {\n // First occurrence wins: a component rendered many times with different\n // props is not the same boundary, and this map answers the coarser\n // question of whether *anything* at that boundary moved.\n if (!digests.has(owner.name)) digests.set(owner.name, owner.propsDigest);\n }\n }\n\n return digests;\n}\n\n/**\n * Loudest band present. A group containing one `a11y` delta is an `a11y` root,\n * whatever else moved alongside it.\n *\n * `texture` is the fallback for the empty case rather than a claim: a root is\n * only built from deltas, so the set is never actually empty here.\n */\nfunction dominantBand(deltas: readonly Delta[]): Band {\n return loudestBand(deltas.map((delta) => delta.band)) ?? 'texture';\n}\n"]}
|
|
1
|
+
{"version":3,"file":"attribution.js","sourceRoot":"","sources":["../../../src/compare/diff/attribution.ts"],"names":[],"mappings":"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","sourcesContent":["import { loudestBand, type Band } from '../band.js';\nimport { aggregateImpact } from '../impact.js';\nimport type { EnvironmentDelta } from '../../format/environment.js';\nimport type { OwnerFrame } from '../../format/provenance.js';\nimport { impactTag, type Delta, type Root, type RootKind } from './delta.js';\nimport type { Matching } from './match.js';\n\n/**\n * Grouping deltas into explanations — the docket's entire value proposition.\n *\n * Separated from `index.ts` because it is the half of diffing that has nothing\n * to do with observation: it takes deltas that already exist and decides which\n * *one* sentence covers them. The precedence rules, the metric-delta folding and\n * the boundary walk are all in service of the same claim — report the largest\n * explanation once rather than the smallest one many times — and they only make\n * sense read together.\n */\n\n/**\n * Group deltas into explanations (spec §6.2).\n *\n * Precedence is fixed and deliberate: environment, then token, then props, then\n * component. Each level explains more subjects than the one below it, and the\n * docket's value is entirely in reporting the *largest* explanation once rather\n * than the smallest one many times.\n */\nexport function attribute(\n deltas: readonly Delta[],\n matching: Matching,\n environmentDeltas: readonly EnvironmentDelta[],\n): readonly Root[] {\n const groups = new Map<string, { kind: RootKind; label: string; cause?: string; deltas: Delta[] }>();\n\n const add = (id: string, kind: RootKind, label: string, delta: Delta, cause?: string): void => {\n const existing = groups.get(id);\n if (existing) existing.deltas.push(delta);\n else groups.set(id, { kind, label, deltas: [delta], ...(cause !== undefined ? { cause } : {}) });\n };\n\n // A changed environment explains everything under it, so it is collapsed to a\n // single root — the one-action re-baselining case of spec §7.3.\n if (environmentDeltas.length > 0) {\n for (const environmentDelta of environmentDeltas) {\n groups.set(`env:${environmentDelta.field}`, {\n kind: 'environment',\n label: `${environmentDelta.field} ${environmentDelta.from} → ${environmentDelta.to}`,\n deltas: [],\n });\n }\n }\n\n const propsBefore = ownerDigests(matching, 'before');\n const propsAfter = ownerDigests(matching, 'after');\n\n const classify = (delta: Delta): Group | null => {\n if (delta.token) return { id: `token:${delta.token}`, kind: 'token', label: delta.token };\n\n const owners = delta.owners;\n if (owners === undefined || owners.length === 0) return null;\n\n const boundary = changedBoundary(owners, propsBefore, propsAfter);\n\n if (boundary === null) {\n // Every incoming props digest held, so the change originated inside a\n // component rather than arriving from outside.\n //\n // *Which* component depends on what kind of change it is. A style change\n // belongs to the component the styled node sits in. A node appearing,\n // vanishing, or moving belongs to whoever *decided it is there* — the\n // component whose JSX created the element, not the component the element\n // is. When a filter list reorders, the nodes that moved are `Chip`s\n // enclosed by a `Stack`, and naming either reports the thing that was\n // rearranged instead of the code that rearranged it.\n const owner = owners[0]!;\n\n // A node that *moved* is the only case where the responsible component is\n // not the one the node belongs to. Ordering is decided by whoever wrote the\n // JSX that placed the element, so a reordered filter list is `TodoFooter`'s\n // change even though every node that moved is a `Chip`.\n //\n // Appearing and disappearing are deliberately *not* treated this way. A\n // component that swaps its own output produces nodes whose creator is that\n // component itself, and crediting whoever placed it would blame its parent\n // for a change it did not make. One swap — `Toggle` rendering a `<div>`\n // where it used to render an `<input>` — is a matched pair and arrives\n // here as `tag-changed` (see `match.ts`); two swaps by one author stay an\n // added and a removed node each, and the same rule names `Toggle` for both.\n //\n // Known cost: when a component element is added or removed wholesale, this\n // names the component that appeared rather than the one that decided to\n // render it. Distinguishing those needs to know whether the component still\n // exists on the other side, which is a question about the change set rather\n // than about the delta.\n const name = (delta.kind === 'node-moved' ? owner.createdBy : undefined) ?? owner.name;\n\n return { id: `component:${name}`, kind: 'component', label: name, cause: name };\n }\n\n // Props moved at a boundary, so the change arrived from outside. The root is\n // the provider — the component that passes props across that boundary.\n const changed = owners[boundary]!;\n const provider = owners[boundary + 1];\n return {\n id: `prop:${provider?.name ?? '?'}>${changed.name}`,\n kind: 'prop',\n label: provider ? `${provider.name} → ${changed.name}` : changed.name,\n // The *provider*, not the component the delta landed in. This is the whole\n // point of a `prop` root: the edit is upstream, and the report has to send\n // a reviewer there.\n //\n // Set **only when a provider exists**. With none, the changed boundary is\n // the outermost frame in the chain: the props arrived from outside the\n // subject entirely, and no component inside it is responsible. Naming the\n // boundary there would send a reviewer to a component whose source is also\n // unchanged, which is the same failure one level out. The fallback in\n // `componentsOf` — the innermost owner, where the change landed — is what\n // both corpora ask for in that case, and the entry's own label still says\n // whose props moved.\n ...(provider !== undefined ? { cause: provider.name } : {}),\n };\n };\n\n // Metric deltas are held back until every cause has a group, then attached to\n // the nearest one. Without this a profile with layout reports a root per\n // component whose box happened to resize — the change is not lost, but \"one\n // root plus counted collateral\" becomes \"one root per affected component\",\n // which is the report spec §6.2 exists to prevent.\n const causes = deltas.filter((delta) => !isMetric(delta)).map((delta) => ({ delta, group: classify(delta) }));\n\n for (const { delta, group } of causes) {\n if (group === null) add('unattributed', 'unattributed', 'no owner chain', delta);\n else add(group.id, group.kind, group.label, delta, group.cause);\n }\n\n for (const delta of deltas) {\n if (!isMetric(delta)) continue;\n\n const target = nearestCause(delta, causes);\n\n if (target !== undefined) {\n add(target.id, target.kind, target.label, delta, target.cause);\n continue;\n }\n\n // No cause anywhere: the box moved and nothing explains it. That is a real\n // finding — most often a change outside the subject reaching in — and it\n // keeps its own root rather than being attached to an unrelated one.\n const own = classify(delta);\n if (own === null) add('unattributed', 'unattributed', 'no owner chain', delta);\n else add(own.id, own.kind, own.label, delta, own.cause);\n }\n\n return [...groups.entries()].map(([id, group]) => ({\n id,\n kind: group.kind,\n label: group.label,\n ...(group.cause !== undefined ? { cause: group.cause } : {}),\n band: dominantBand(group.deltas),\n impact: aggregateImpact(group.deltas.map(impactTag)),\n deltas: group.deltas,\n }));\n}\n\ninterface Group {\n readonly id: string;\n readonly kind: RootKind;\n readonly label: string;\n /** The component responsible. See `Root.cause`. */\n readonly cause?: string;\n}\n\n/**\n * Computed properties that are functions of the used box rather than of any\n * declaration.\n *\n * A layout engine reports these for every element whether or not a stylesheet\n * mentioned them, so they move whenever anything inside or above the node\n * changes size. Treating them as causes is what turned one token edit into five\n * docket entries under `chromium` — `band.ts` already says the attributor must\n * fold rect movement into the style change as collateral, and these are the\n * `style-changed` half of the same evidence.\n *\n * Note the cost of the simplification: a genuine `width: 100px → 200px` edit is\n * folded too, whenever the same node carries another delta to fold into. That\n * loses no delta and moves no verdict — the change is still reported, under a\n * root that is a strictly better description of the cause.\n */\nconst USED_VALUE_PROPERTIES: ReadonlySet<string> = new Set([\n 'width', 'height', 'inline-size', 'block-size',\n 'transform-origin', 'perspective-origin',\n]);\n\n/** Evidence that a box moved, as opposed to evidence of why. */\nfunction isMetric(delta: Delta): boolean {\n if (delta.kind === 'rect-changed') return true;\n return delta.kind === 'style-changed' && USED_VALUE_PROPERTIES.has(delta.property ?? '');\n}\n\n/** Paths are `/`-joined child indices, so containment is a prefix test. */\nfunction isDescendant(candidate: string, ancestor: string): boolean {\n return candidate.startsWith(`${ancestor}/`);\n}\n\n/**\n * The cause a metric delta belongs to: same node first, then the nearest change\n * *inside* it, then the nearest change above it.\n *\n * Inside before above, because a box that grew did so because of its contents\n * far more often than because of its container — and when a container really is\n * the cause, its own metric delta folds upward on the same rule, so the two\n * meet at the same root either way.\n */\nfunction nearestCause(\n delta: Delta,\n causes: readonly { readonly delta: Delta; readonly group: Group | null }[],\n): Group | undefined {\n const grouped = causes.filter((cause) => cause.group !== null);\n\n const here = grouped.find((cause) => cause.delta.path === delta.path);\n if (here) return here.group ?? undefined;\n\n const inside = grouped\n .filter((cause) => isDescendant(cause.delta.path, delta.path))\n .sort((a, b) => a.delta.path.length - b.delta.path.length)[0];\n if (inside) return inside.group ?? undefined;\n\n const above = grouped\n .filter((cause) => isDescendant(delta.path, cause.delta.path))\n .sort((a, b) => b.delta.path.length - a.delta.path.length)[0];\n\n return above?.group ?? undefined;\n}\n\n/**\n * The outermost owner whose incoming props changed, or `null` if none did.\n *\n * Outermost rather than innermost: if `CheckoutPage` passes a new prop to\n * `Header`, which passes it to `Button`, all three boundaries moved, and the\n * useful root is the one furthest up — reporting `Button` would name the\n * messenger.\n */\nfunction changedBoundary(\n owners: readonly OwnerFrame[],\n before: ReadonlyMap<string, string>,\n after: ReadonlyMap<string, string>,\n): number | null {\n let outermost: number | null = null;\n\n for (let index = 0; index < owners.length; index += 1) {\n const owner = owners[index]!;\n const digestBefore = before.get(owner.name);\n // An owner absent from the baseline is new structure, not a prop change;\n // the structural delta covers it and inventing a prop root would double-count.\n if (digestBefore === undefined) continue;\n if (digestBefore !== after.get(owner.name)) outermost = index;\n }\n\n return outermost;\n}\n\nfunction ownerDigests(\n matching: Matching,\n side: 'before' | 'after',\n): ReadonlyMap<string, string> {\n const digests = new Map<string, string>();\n\n for (const [before, after] of matching.pairs) {\n const node = side === 'before' ? before : after;\n for (const owner of node.provenance?.owners ?? []) {\n // First occurrence wins: a component rendered many times with different\n // props is not the same boundary, and this map answers the coarser\n // question of whether *anything* at that boundary moved.\n if (!digests.has(owner.name)) digests.set(owner.name, owner.propsDigest);\n }\n }\n\n return digests;\n}\n\n/**\n * Loudest band present. A group containing one `a11y` delta is an `a11y` root,\n * whatever else moved alongside it.\n *\n * `texture` is the fallback for the empty case rather than a claim: a root is\n * only built from deltas, so the set is never actually empty here.\n */\nfunction dominantBand(deltas: readonly Delta[]): Band {\n return loudestBand(deltas.map((delta) => delta.band)) ?? 'texture';\n}\n"]}
|
|
@@ -32,13 +32,21 @@ function describe(node) {
|
|
|
32
32
|
export function compareNodes(before, after, deltas, hasLayout) {
|
|
33
33
|
const owners = after.provenance?.owners;
|
|
34
34
|
const createdBy = after.provenance?.createdBy;
|
|
35
|
+
const retagged = before.tag !== after.tag;
|
|
35
36
|
const base = (kind) => ({
|
|
36
37
|
kind,
|
|
37
|
-
band: bandOf(kind),
|
|
38
|
+
band: bandOf(kind, { retagged }),
|
|
38
39
|
path: after.path,
|
|
39
40
|
...(owners ? { owners } : {}),
|
|
40
41
|
...(createdBy ? { createdBy } : {}),
|
|
41
42
|
});
|
|
43
|
+
// Two tags pair as the two roots, under a declared alias, under one role and
|
|
44
|
+
// name, or as the one replacement its author recorded (see `match.ts`). The tag is said first: the
|
|
45
|
+
// role, the attributes and the box that follow are what the new element is,
|
|
46
|
+
// not edits to the old one.
|
|
47
|
+
if (retagged) {
|
|
48
|
+
deltas.push({ ...base('tag-changed'), from: before.tag, to: after.tag });
|
|
49
|
+
}
|
|
42
50
|
if (before.role !== after.role) {
|
|
43
51
|
deltas.push({ ...base('role-changed'), from: before.role, to: after.role });
|
|
44
52
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compare-nodes.js","sourceRoot":"","sources":["../../../src/compare/diff/compare-nodes.ts"],"names":[],"mappings":"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","sourcesContent":["import { bandOf, type DeltaKind } from '../band.js';\nimport { impactOf } from '../impact.js';\nimport type { SemanticNode } from '../../format/snapshot.js';\nimport type { Delta } from './delta.js';\n\n/**\n * Delta production for a single node, or a single matched pair of nodes.\n *\n * Separated from `index.ts` because everything here answers a strictly local\n * question — given two nodes already known to be the same node, which fields\n * moved — and knows nothing about trees, roots, or attribution. The token\n * reasoning lives here rather than with the attributor for the same reason: the\n * evidence that decides whether a property change is collateral of a token edit\n * is the pair of nodes, and nothing further out has it.\n */\n\nexport function wholeNode(\n kind: 'node-added' | 'node-removed' | 'node-moved',\n node: SemanticNode,\n): Delta {\n return {\n kind,\n band: bandOf(kind),\n path: node.path,\n to: describe(node),\n ...(node.provenance ? { owners: node.provenance.owners } : {}),\n ...(node.provenance?.createdBy ? { createdBy: node.provenance.createdBy } : {}),\n };\n}\n\nfunction describe(node: SemanticNode): string {\n if (node.role !== undefined && node.name !== undefined) return `${node.role} \"${node.name}\"`;\n if (node.role !== undefined) return node.role;\n if (node.text !== undefined) return `${node.tag} \"${node.text}\"`;\n return node.tag;\n}\n\nexport function compareNodes(\n before: SemanticNode,\n after: SemanticNode,\n deltas: Delta[],\n hasLayout: boolean,\n): void {\n const owners = after.provenance?.owners;\n const createdBy = after.provenance?.createdBy;\n const base = (kind: DeltaKind) => ({\n kind,\n band: bandOf(kind),\n path: after.path,\n ...(owners ? { owners } : {}),\n ...(createdBy ? { createdBy } : {}),\n });\n\n if (before.role !== after.role) {\n deltas.push({ ...base('role-changed'), from: before.role, to: after.role });\n }\n if (before.name !== after.name) {\n deltas.push({ ...base('name-changed'), from: before.name, to: after.name });\n }\n if (before.description !== after.description) {\n deltas.push({\n ...base('description-changed'),\n from: before.description,\n to: after.description,\n });\n }\n if (before.text !== after.text) {\n deltas.push({ ...base('text-changed'), from: before.text, to: after.text });\n }\n\n for (const property of unionKeys(before.state, after.state)) {\n const from = before.state?.[property];\n const to = after.state?.[property];\n if (from !== to) {\n deltas.push({ ...base('state-changed'), property, from: str(from), to: str(to) });\n }\n }\n\n for (const property of unionKeys(before.attributes, after.attributes)) {\n const from = before.attributes[property];\n const to = after.attributes[property];\n if (from !== to) {\n deltas.push({ ...base('attribute-changed'), property, from, to });\n }\n }\n\n let reflowCause: Delta | null = null;\n\n for (const property of unionKeys(before.style, after.style)) {\n const from = before.style[property];\n const to = after.style[property];\n if (from === to) continue;\n\n // A token whose own value moved makes this delta collateral rather than a\n // root. Recorded here so attribution does not have to re-derive it.\n const token = changedTokenFor(before, after, property);\n const impact = impactOf(property);\n\n const delta: Delta = {\n ...base('style-changed'),\n property,\n from,\n to,\n impact,\n ...(token ? { token } : {}),\n };\n\n deltas.push(delta);\n if (impact === 'layout' && reflowCause === null) reflowCause = delta;\n }\n\n // Which token a property resolves *through* is part of the render hash, so a\n // change in it moves the hash — but under a profile that cannot resolve custom\n // properties both resolved values are the same empty string and the loop above\n // sees nothing. The result was a non-identical diff carrying zero deltas and\n // zero roots: a correct verdict with an empty docket, which tells a reviewer\n // that something changed and then refuses to say what. Found by scoring\n // `prop-size/button` under `jsdom`, which only became scorable with ADR-0008.\n //\n // Skipped when the resolved value moved too: the `style-changed` delta above\n // already names that property, and reporting both splits one cause in two.\n for (const property of unionKeys(before.styleTokens, after.styleTokens)) {\n const from = before.styleTokens?.[property];\n const to = after.styleTokens?.[property];\n if (from === to) continue;\n if (before.style[property] !== after.style[property]) continue;\n\n deltas.push({ ...base('token-changed'), property, from, to });\n }\n\n if (hasLayout && !sameRect(before, after)) {\n // A rect that moved because this node's own padding changed is the same\n // finding observed twice. Folding it under its cause — and inheriting that\n // cause's token — keeps one edit as one docket entry instead of splitting it\n // into a `token` root and an unrelated-looking `geometry` one.\n //\n // A rect that moved with *no* layout-impact change here is different and\n // stays independent: something upstream reflowed and pushed this node, which\n // is exactly the propagation worth surfacing.\n deltas.push({\n ...base('rect-changed'),\n ...(before.rect ? { rectFrom: before.rect } : {}),\n ...(after.rect ? { rectTo: after.rect } : {}),\n ...(reflowCause\n ? {\n derivedFrom: `${reflowCause.path}:${reflowCause.property ?? ''}`,\n ...(reflowCause.token ? { token: reflowCause.token } : {}),\n }\n : {}),\n });\n }\n}\n\n/**\n * The token that explains *this property's* change, if one does.\n *\n * Two conditions, and both are load-bearing. The property must actually resolve\n * through the token — a node whose `color` comes from a token and whose padding\n * comes from a literal is collateral of a token edit only in its colour. And the\n * token's own value must have moved: a node that merely mentions\n * `--color-primary` while that token held is not collateral of anything, its own\n * rule changed, and it is a root.\n *\n * Dropping either check splits one docket entry into several. Ignoring the\n * property meant a rule that overrode a token-driven value got attributed to the\n * token it had just stopped using.\n */\nfunction changedTokenFor(\n before: SemanticNode,\n after: SemanticNode,\n property: string,\n): string | undefined {\n const token = before.styleTokens?.[property];\n\n // The *same* token must drive the property on both sides. A property that\n // stopped resolving through a token did so because some rule started winning\n // instead — that rule is the root, and blaming the abandoned token would name\n // the thing that did not change.\n // Properties whose initial value is `currentColor` follow `color` without ever\n // naming it: no rule declares `outline-color`, so it resolves through no token,\n // yet a token-driven `color` change moves it. Under a profile with computed\n // style that produced a second root per Button — \"the accent token moved\" and\n // \"Button changed\" — for one edit. Recognised by the value matching `color` on\n // both sides, so a node that genuinely declares its own outline colour is\n // unaffected.\n if (token === undefined && FOLLOWS_CURRENT_COLOR.has(property)) {\n const from = before.style[property];\n const to = after.style[property];\n if (from !== undefined && to !== undefined && from === before.style['color'] && to === after.style['color']) {\n return changedTokenFor(before, after, 'color');\n }\n return undefined;\n }\n\n if (token === undefined || after.styleTokens?.[property] !== token) return undefined;\n\n // Undefined on one side counts as a change: a theme override introducing a\n // token that previously had no value is exactly the case the token band exists\n // to collapse into one root.\n if (before.tokens?.[token] !== after.tokens?.[token]) return token;\n\n // The named token held — and may never have had a value at all.\n // `var(--ks-card-radius, var(--va-radius-md))` is the ordinary shape of a\n // component token with a system fallback: `styleTokens` names the outer one,\n // because that is what the author wrote, and when it is undefined the value\n // came from the fallback. Comparing the outer name's value then compares\n // `undefined` with `undefined`, concludes the token held, and reports an edit\n // to the radius *scale* as `component:Card` — \"Card changed internally\", once\n // per consuming component — instead of one token root with counted collateral.\n // That is the product's headline claim failing on the most common\n // design-system shape there is, and it survived because the corpus asserted\n // the root *count*, which is one either way.\n return soleMovedToken(before, after);\n}\n\n/**\n * The one token this node resolved through whose value moved, if there is\n * exactly one.\n *\n * The fallback for a chain whose outer name carries no value. Deliberately\n * refuses to answer when two moved: \"the named token held and several others\n * changed\" is genuinely ambiguous, and picking one would put a confident wrong\n * name in front of a reviewer. Ambiguity falls through to the component root,\n * which is coarser and true.\n */\nfunction soleMovedToken(before: SemanticNode, after: SemanticNode): string | undefined {\n const moved = unionKeys(before.tokens, after.tokens).filter(\n (name) => before.tokens?.[name] !== after.tokens?.[name],\n );\n\n return moved.length === 1 ? moved[0] : undefined;\n}\n\n/**\n * Properties whose initial value is `currentColor`.\n *\n * `color` itself is excluded, or the lookup would recurse.\n */\nconst FOLLOWS_CURRENT_COLOR: ReadonlySet<string> = new Set([\n 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',\n 'outline-color', 'text-decoration-color', 'text-emphasis-color', 'column-rule-color',\n 'caret-color',\n]);\n\nfunction sameRect(before: SemanticNode, after: SemanticNode): boolean {\n const a = before.rect;\n const b = after.rect;\n if (a === undefined || b === undefined) return a === b;\n return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;\n}\n\nfunction unionKeys(\n a: Readonly<Record<string, unknown>> | undefined,\n b: Readonly<Record<string, unknown>> | undefined,\n): readonly string[] {\n return [...new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})])].sort();\n}\n\nfunction str(value: unknown): string | undefined {\n return value === undefined ? undefined : String(value);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"compare-nodes.js","sourceRoot":"","sources":["../../../src/compare/diff/compare-nodes.ts"],"names":[],"mappings":"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","sourcesContent":["import { bandOf, type DeltaKind } from '../band.js';\nimport { impactOf } from '../impact.js';\nimport type { SemanticNode } from '../../format/snapshot.js';\nimport type { Delta } from './delta.js';\n\n/**\n * Delta production for a single node, or a single matched pair of nodes.\n *\n * Separated from `index.ts` because everything here answers a strictly local\n * question — given two nodes already known to be the same node, which fields\n * moved — and knows nothing about trees, roots, or attribution. The token\n * reasoning lives here rather than with the attributor for the same reason: the\n * evidence that decides whether a property change is collateral of a token edit\n * is the pair of nodes, and nothing further out has it.\n */\n\nexport function wholeNode(\n kind: 'node-added' | 'node-removed' | 'node-moved',\n node: SemanticNode,\n): Delta {\n return {\n kind,\n band: bandOf(kind),\n path: node.path,\n to: describe(node),\n ...(node.provenance ? { owners: node.provenance.owners } : {}),\n ...(node.provenance?.createdBy ? { createdBy: node.provenance.createdBy } : {}),\n };\n}\n\nfunction describe(node: SemanticNode): string {\n if (node.role !== undefined && node.name !== undefined) return `${node.role} \"${node.name}\"`;\n if (node.role !== undefined) return node.role;\n if (node.text !== undefined) return `${node.tag} \"${node.text}\"`;\n return node.tag;\n}\n\nexport function compareNodes(\n before: SemanticNode,\n after: SemanticNode,\n deltas: Delta[],\n hasLayout: boolean,\n): void {\n const owners = after.provenance?.owners;\n const createdBy = after.provenance?.createdBy;\n const retagged = before.tag !== after.tag;\n const base = (kind: DeltaKind) => ({\n kind,\n band: bandOf(kind, { retagged }),\n path: after.path,\n ...(owners ? { owners } : {}),\n ...(createdBy ? { createdBy } : {}),\n });\n\n // Two tags pair as the two roots, under a declared alias, under one role and\n // name, or as the one replacement its author recorded (see `match.ts`). The tag is said first: the\n // role, the attributes and the box that follow are what the new element is,\n // not edits to the old one.\n if (retagged) {\n deltas.push({ ...base('tag-changed'), from: before.tag, to: after.tag });\n }\n\n if (before.role !== after.role) {\n deltas.push({ ...base('role-changed'), from: before.role, to: after.role });\n }\n if (before.name !== after.name) {\n deltas.push({ ...base('name-changed'), from: before.name, to: after.name });\n }\n if (before.description !== after.description) {\n deltas.push({\n ...base('description-changed'),\n from: before.description,\n to: after.description,\n });\n }\n if (before.text !== after.text) {\n deltas.push({ ...base('text-changed'), from: before.text, to: after.text });\n }\n\n for (const property of unionKeys(before.state, after.state)) {\n const from = before.state?.[property];\n const to = after.state?.[property];\n if (from !== to) {\n deltas.push({ ...base('state-changed'), property, from: str(from), to: str(to) });\n }\n }\n\n for (const property of unionKeys(before.attributes, after.attributes)) {\n const from = before.attributes[property];\n const to = after.attributes[property];\n if (from !== to) {\n deltas.push({ ...base('attribute-changed'), property, from, to });\n }\n }\n\n let reflowCause: Delta | null = null;\n\n for (const property of unionKeys(before.style, after.style)) {\n const from = before.style[property];\n const to = after.style[property];\n if (from === to) continue;\n\n // A token whose own value moved makes this delta collateral rather than a\n // root. Recorded here so attribution does not have to re-derive it.\n const token = changedTokenFor(before, after, property);\n const impact = impactOf(property);\n\n const delta: Delta = {\n ...base('style-changed'),\n property,\n from,\n to,\n impact,\n ...(token ? { token } : {}),\n };\n\n deltas.push(delta);\n if (impact === 'layout' && reflowCause === null) reflowCause = delta;\n }\n\n // Which token a property resolves *through* is part of the render hash, so a\n // change in it moves the hash — but under a profile that cannot resolve custom\n // properties both resolved values are the same empty string and the loop above\n // sees nothing. The result was a non-identical diff carrying zero deltas and\n // zero roots: a correct verdict with an empty docket, which tells a reviewer\n // that something changed and then refuses to say what. Found by scoring\n // `prop-size/button` under `jsdom`, which only became scorable with ADR-0008.\n //\n // Skipped when the resolved value moved too: the `style-changed` delta above\n // already names that property, and reporting both splits one cause in two.\n for (const property of unionKeys(before.styleTokens, after.styleTokens)) {\n const from = before.styleTokens?.[property];\n const to = after.styleTokens?.[property];\n if (from === to) continue;\n if (before.style[property] !== after.style[property]) continue;\n\n deltas.push({ ...base('token-changed'), property, from, to });\n }\n\n if (hasLayout && !sameRect(before, after)) {\n // A rect that moved because this node's own padding changed is the same\n // finding observed twice. Folding it under its cause — and inheriting that\n // cause's token — keeps one edit as one docket entry instead of splitting it\n // into a `token` root and an unrelated-looking `geometry` one.\n //\n // A rect that moved with *no* layout-impact change here is different and\n // stays independent: something upstream reflowed and pushed this node, which\n // is exactly the propagation worth surfacing.\n deltas.push({\n ...base('rect-changed'),\n ...(before.rect ? { rectFrom: before.rect } : {}),\n ...(after.rect ? { rectTo: after.rect } : {}),\n ...(reflowCause\n ? {\n derivedFrom: `${reflowCause.path}:${reflowCause.property ?? ''}`,\n ...(reflowCause.token ? { token: reflowCause.token } : {}),\n }\n : {}),\n });\n }\n}\n\n/**\n * The token that explains *this property's* change, if one does.\n *\n * Two conditions, and both are load-bearing. The property must actually resolve\n * through the token — a node whose `color` comes from a token and whose padding\n * comes from a literal is collateral of a token edit only in its colour. And the\n * token's own value must have moved: a node that merely mentions\n * `--color-primary` while that token held is not collateral of anything, its own\n * rule changed, and it is a root.\n *\n * Dropping either check splits one docket entry into several. Ignoring the\n * property meant a rule that overrode a token-driven value got attributed to the\n * token it had just stopped using.\n */\nfunction changedTokenFor(\n before: SemanticNode,\n after: SemanticNode,\n property: string,\n): string | undefined {\n const token = before.styleTokens?.[property];\n\n // The *same* token must drive the property on both sides. A property that\n // stopped resolving through a token did so because some rule started winning\n // instead — that rule is the root, and blaming the abandoned token would name\n // the thing that did not change.\n // Properties whose initial value is `currentColor` follow `color` without ever\n // naming it: no rule declares `outline-color`, so it resolves through no token,\n // yet a token-driven `color` change moves it. Under a profile with computed\n // style that produced a second root per Button — \"the accent token moved\" and\n // \"Button changed\" — for one edit. Recognised by the value matching `color` on\n // both sides, so a node that genuinely declares its own outline colour is\n // unaffected.\n if (token === undefined && FOLLOWS_CURRENT_COLOR.has(property)) {\n const from = before.style[property];\n const to = after.style[property];\n if (from !== undefined && to !== undefined && from === before.style['color'] && to === after.style['color']) {\n return changedTokenFor(before, after, 'color');\n }\n return undefined;\n }\n\n if (token === undefined || after.styleTokens?.[property] !== token) return undefined;\n\n // Undefined on one side counts as a change: a theme override introducing a\n // token that previously had no value is exactly the case the token band exists\n // to collapse into one root.\n if (before.tokens?.[token] !== after.tokens?.[token]) return token;\n\n // The named token held — and may never have had a value at all.\n // `var(--ks-card-radius, var(--va-radius-md))` is the ordinary shape of a\n // component token with a system fallback: `styleTokens` names the outer one,\n // because that is what the author wrote, and when it is undefined the value\n // came from the fallback. Comparing the outer name's value then compares\n // `undefined` with `undefined`, concludes the token held, and reports an edit\n // to the radius *scale* as `component:Card` — \"Card changed internally\", once\n // per consuming component — instead of one token root with counted collateral.\n // That is the product's headline claim failing on the most common\n // design-system shape there is, and it survived because the corpus asserted\n // the root *count*, which is one either way.\n return soleMovedToken(before, after);\n}\n\n/**\n * The one token this node resolved through whose value moved, if there is\n * exactly one.\n *\n * The fallback for a chain whose outer name carries no value. Deliberately\n * refuses to answer when two moved: \"the named token held and several others\n * changed\" is genuinely ambiguous, and picking one would put a confident wrong\n * name in front of a reviewer. Ambiguity falls through to the component root,\n * which is coarser and true.\n */\nfunction soleMovedToken(before: SemanticNode, after: SemanticNode): string | undefined {\n const moved = unionKeys(before.tokens, after.tokens).filter(\n (name) => before.tokens?.[name] !== after.tokens?.[name],\n );\n\n return moved.length === 1 ? moved[0] : undefined;\n}\n\n/**\n * Properties whose initial value is `currentColor`.\n *\n * `color` itself is excluded, or the lookup would recurse.\n */\nconst FOLLOWS_CURRENT_COLOR: ReadonlySet<string> = new Set([\n 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',\n 'outline-color', 'text-decoration-color', 'text-emphasis-color', 'column-rule-color',\n 'caret-color',\n]);\n\nfunction sameRect(before: SemanticNode, after: SemanticNode): boolean {\n const a = before.rect;\n const b = after.rect;\n if (a === undefined || b === undefined) return a === b;\n return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;\n}\n\nfunction unionKeys(\n a: Readonly<Record<string, unknown>> | undefined,\n b: Readonly<Record<string, unknown>> | undefined,\n): readonly string[] {\n return [...new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})])].sort();\n}\n\nfunction str(value: unknown): string | undefined {\n return value === undefined ? undefined : String(value);\n}\n"]}
|