@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,312 @@
|
|
|
1
|
+
import { BANDS } from '../compare/band.js';
|
|
2
|
+
import { digestValue } from '../format/hash.js';
|
|
3
|
+
import { boundaries, shapeOf, UNATTRIBUTED } from './boundary.js';
|
|
4
|
+
export { UNATTRIBUTED } from './boundary.js';
|
|
5
|
+
/**
|
|
6
|
+
* Per-component content hashes, one per band.
|
|
7
|
+
*
|
|
8
|
+
* `SemanticSnapshot` hashes a whole subject. That answers "did anything change"
|
|
9
|
+
* and nothing else: it moves whenever anything inside it moves, so it cannot say
|
|
10
|
+
* *which area*, and a record built on it says "the page changed" on every commit.
|
|
11
|
+
*
|
|
12
|
+
* These hashes are the unit a history is kept in. Text, and small — a component
|
|
13
|
+
* that did not change contributes the same digest it did last time, so the only
|
|
14
|
+
* thing worth recording is the difference.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Hash every component boundary in a subject.
|
|
18
|
+
*
|
|
19
|
+
* Ordered by component name so an unchanged subject produces byte-identical
|
|
20
|
+
* output across runs.
|
|
21
|
+
*
|
|
22
|
+
* The walk and the per-boundary shape live in {@link ./boundary.js}, which
|
|
23
|
+
* {@link ./instances.js} shares — one definition of what a component's own
|
|
24
|
+
* content is, read by the aggregate and by the per-instance form.
|
|
25
|
+
*/
|
|
26
|
+
export function hashComponents(snapshot) {
|
|
27
|
+
const layout = snapshot.profile.layout;
|
|
28
|
+
const accumulated = new Map();
|
|
29
|
+
for (const boundary of boundaries(snapshot.root)) {
|
|
30
|
+
const shape = shapeOf(boundary, layout);
|
|
31
|
+
const entry = accumulated.get(boundary.component) ?? {
|
|
32
|
+
structure: [],
|
|
33
|
+
semantics: [],
|
|
34
|
+
text: [],
|
|
35
|
+
style: [],
|
|
36
|
+
geometry: [],
|
|
37
|
+
boxes: [],
|
|
38
|
+
};
|
|
39
|
+
entry.structure.push(shape.structure);
|
|
40
|
+
entry.semantics.push(shape.semantics);
|
|
41
|
+
entry.text.push(shape.text);
|
|
42
|
+
entry.style.push(shape.style);
|
|
43
|
+
entry.geometry.push(shape.geometry);
|
|
44
|
+
entry.boxes.push(shape.box);
|
|
45
|
+
accumulated.set(boundary.component, entry);
|
|
46
|
+
}
|
|
47
|
+
return [...accumulated.entries()]
|
|
48
|
+
.map(([component, entry]) => ({
|
|
49
|
+
component,
|
|
50
|
+
instances: entry.structure.length,
|
|
51
|
+
structure: digestValue(entry.structure),
|
|
52
|
+
semantics: digestValue(entry.semantics),
|
|
53
|
+
text: digestValue(entry.text),
|
|
54
|
+
style: digestValue(entry.style),
|
|
55
|
+
...(layout ? { geometry: digestValue(entry.geometry) } : {}),
|
|
56
|
+
// Under a profile with no layout every entry is `null`, and a list of
|
|
57
|
+
// nulls is a measurement nobody took written as one they did. Absent
|
|
58
|
+
// instead, which is the same rule `geometry` above obeys.
|
|
59
|
+
...(layout ? { boxes: entry.boxes } : {}),
|
|
60
|
+
}))
|
|
61
|
+
// Code-unit order, not `localeCompare`. The doc above promises byte-identical
|
|
62
|
+
// output for an unchanged subject, and a locale-aware comparison makes that a
|
|
63
|
+
// promise about the machine's `LANG` — which was tolerable while these were
|
|
64
|
+
// internal and is not now that they are written into a sidecar, committed
|
|
65
|
+
// beside a baseline, and read back on someone else's runner.
|
|
66
|
+
.sort((a, b) => (a.component < b.component ? -1 : a.component > b.component ? 1 : 0));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Which components *caused* a change, given both revisions' hashes.
|
|
70
|
+
*
|
|
71
|
+
* The arithmetic behind cause-first ranking, and the reason a baseline carries
|
|
72
|
+
* its component hashes at all. `rankRegions` takes a list of cause names and,
|
|
73
|
+
* given none, falls back to area — which
|
|
74
|
+
* [journal 0013](../../../../docs/context/journal/0013-observability.md)
|
|
75
|
+
* measured as backwards by 6×, because area measures displacement rather than
|
|
76
|
+
* cause. This is where that list comes from on a path with no second document.
|
|
77
|
+
*
|
|
78
|
+
* The distinction is the split ADR-0018 built the bands for:
|
|
79
|
+
*
|
|
80
|
+
* - **`structure` or `style` moved** — the component's own content is different.
|
|
81
|
+
* It is a cause.
|
|
82
|
+
* - **only `geometry` moved** — the component is byte-identical and its box is
|
|
83
|
+
* somewhere else. Something *else* moved it, so it is collateral, and naming
|
|
84
|
+
* it would send a reviewer to a file nobody edited.
|
|
85
|
+
* - **appeared or disappeared** — a component present on one side only is a
|
|
86
|
+
* cause. Something decided to render it or to stop.
|
|
87
|
+
*
|
|
88
|
+
* `instances` is deliberately not consulted. A component rendered five times
|
|
89
|
+
* instead of four has a different `structure` digest for the subject, and the
|
|
90
|
+
* count moving on its own — the same component, the same content, one more of
|
|
91
|
+
* them — is a change in whatever decided how many, not in this component.
|
|
92
|
+
*
|
|
93
|
+
* A profile with no layout supplies no `geometry`, so on that tier every
|
|
94
|
+
* difference is `structure` or `style` and every changed component is a cause.
|
|
95
|
+
* That is correct rather than degraded: with no boxes, nothing was displaced.
|
|
96
|
+
*/
|
|
97
|
+
export function causesBetween(before, after) {
|
|
98
|
+
const previous = new Map(before.map((entry) => [entry.component, entry]));
|
|
99
|
+
const causes = [];
|
|
100
|
+
for (const entry of after) {
|
|
101
|
+
// `(unattributed)` is not a component and can never be a cause. It is the
|
|
102
|
+
// bucket for nodes whose provenance chain broke, so it collects unrelated
|
|
103
|
+
// parts of a page under one name — and nothing downstream could act on it
|
|
104
|
+
// anyway: a region with no owner reports no component, so it would never
|
|
105
|
+
// match. A broken chain is a defect in this tool and is reported as
|
|
106
|
+
// `unattributed` where that means something, not smuggled in here as a
|
|
107
|
+
// culprit.
|
|
108
|
+
if (entry.component === UNATTRIBUTED)
|
|
109
|
+
continue;
|
|
110
|
+
const was = previous.get(entry.component);
|
|
111
|
+
if (was === undefined) {
|
|
112
|
+
causes.push(entry.component);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (ownContentMoved(was, entry))
|
|
116
|
+
causes.push(entry.component);
|
|
117
|
+
}
|
|
118
|
+
// Removals too, and they are the case a candidate-only walk cannot see: a
|
|
119
|
+
// component that stopped rendering leaves regions behind it, and the component
|
|
120
|
+
// that used to be there is exactly the name a reviewer needs.
|
|
121
|
+
const present = new Set(after.map((entry) => entry.component));
|
|
122
|
+
for (const entry of before) {
|
|
123
|
+
if (entry.component === UNATTRIBUTED)
|
|
124
|
+
continue;
|
|
125
|
+
if (!present.has(entry.component))
|
|
126
|
+
causes.push(entry.component);
|
|
127
|
+
}
|
|
128
|
+
return causes.sort();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Whether a component's *own content* differs, ignoring where its box ended up.
|
|
132
|
+
*
|
|
133
|
+
* Deliberately not phrased in bands, because it is not a band question. Both
|
|
134
|
+
* `structure` and `geometry` map to the `geometry` band, and this has to keep
|
|
135
|
+
* them apart: a component whose tree changed edited itself, and a component
|
|
136
|
+
* whose rect moved was pushed. That distinction is the entire cause/collateral
|
|
137
|
+
* result, and asking it through the band mapping would need the digests back
|
|
138
|
+
* again to answer it.
|
|
139
|
+
*
|
|
140
|
+
* The list is every digest a component owns except `geometry`. It grew by two on
|
|
141
|
+
* 2026-08-06 without changing meaning: `semantics` and `text` used to be inside
|
|
142
|
+
* `structure`.
|
|
143
|
+
*/
|
|
144
|
+
function ownContentMoved(before, after) {
|
|
145
|
+
return (before.structure !== after.structure ||
|
|
146
|
+
before.semantics !== after.semantics ||
|
|
147
|
+
before.text !== after.text ||
|
|
148
|
+
before.style !== after.style);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Which frequency bands moved between one component's two hashes.
|
|
152
|
+
*
|
|
153
|
+
* The reason the digests were split. A baseline carries hashes and not
|
|
154
|
+
* documents, so "what changed here" used to be answerable only as a boolean —
|
|
155
|
+
* and a boolean cannot serve a route-level test, whose entire request is *tell
|
|
156
|
+
* me when the page stops assembling and never when it is repainted*.
|
|
157
|
+
*
|
|
158
|
+
* The mapping is exact and it is the same one `bandOf` applies to a delta, which
|
|
159
|
+
* is the property that matters: a subject relaxed to `layout` must absorb the
|
|
160
|
+
* same things whether the run held two documents or two sidecars. Two mappings
|
|
161
|
+
* would be one drift away from a config key meaning different things on the two
|
|
162
|
+
* paths, discovered as a regression somebody let through.
|
|
163
|
+
*
|
|
164
|
+
* | digest | band | what it covers |
|
|
165
|
+
* |---|---|---|
|
|
166
|
+
* | `semantics` | `a11y` | role, accessible name, ARIA state |
|
|
167
|
+
* | `text` | `content` | text runs |
|
|
168
|
+
* | `structure` | `geometry` | tags, aliases, attributes, child boundaries |
|
|
169
|
+
* | `geometry` | `geometry` | rects and computed layout output |
|
|
170
|
+
* | `style` | `token` | declared values and custom properties |
|
|
171
|
+
*
|
|
172
|
+
* `texture` never appears. It is raster residue by definition, and a component
|
|
173
|
+
* hash is built from a document — so the band a comparison of hashes cannot
|
|
174
|
+
* decide is *absent* from the answer rather than reported as unmoved, which is
|
|
175
|
+
* ADR-0002's rule applied to a narrower question.
|
|
176
|
+
*
|
|
177
|
+
* A missing `geometry` on either side is the profile saying it has no layout
|
|
178
|
+
* engine, and is not a difference. Treating absent as a change would report
|
|
179
|
+
* every component as having moved the moment a jsdom baseline met a Chromium
|
|
180
|
+
* run — which the environment key already refuses as `incomparable`, so this
|
|
181
|
+
* would be a second, wronger answer to a question already settled.
|
|
182
|
+
*/
|
|
183
|
+
export function movedBands(before, after) {
|
|
184
|
+
const moved = new Set();
|
|
185
|
+
if (before.semantics !== after.semantics)
|
|
186
|
+
moved.add('a11y');
|
|
187
|
+
if (before.text !== after.text)
|
|
188
|
+
moved.add('content');
|
|
189
|
+
if (before.structure !== after.structure)
|
|
190
|
+
moved.add('geometry');
|
|
191
|
+
if (before.style !== after.style)
|
|
192
|
+
moved.add('token');
|
|
193
|
+
if (before.geometry !== undefined &&
|
|
194
|
+
after.geometry !== undefined &&
|
|
195
|
+
before.geometry !== after.geometry) {
|
|
196
|
+
moved.add('geometry');
|
|
197
|
+
}
|
|
198
|
+
return BANDS.filter((band) => moved.has(band));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Every band that moved anywhere in the subject, given both revisions' hashes.
|
|
202
|
+
*
|
|
203
|
+
* A component present on one side only contributes `geometry`: something was
|
|
204
|
+
* added or removed, which is the structural half of that band however the rest
|
|
205
|
+
* of it compares. It deliberately does not contribute `a11y` or `content` as
|
|
206
|
+
* well — a component that is simply not there did not *rename* anything, and
|
|
207
|
+
* inflating the answer would make a level that absorbs nothing look like the
|
|
208
|
+
* only safe choice.
|
|
209
|
+
*/
|
|
210
|
+
export function bandsBetween(before, after) {
|
|
211
|
+
const previous = new Map(before.map((entry) => [entry.component, entry]));
|
|
212
|
+
const present = new Set(after.map((entry) => entry.component));
|
|
213
|
+
const moved = new Set();
|
|
214
|
+
for (const entry of after) {
|
|
215
|
+
const was = previous.get(entry.component);
|
|
216
|
+
if (was === undefined)
|
|
217
|
+
moved.add('geometry');
|
|
218
|
+
else
|
|
219
|
+
for (const band of movedBands(was, entry))
|
|
220
|
+
moved.add(band);
|
|
221
|
+
}
|
|
222
|
+
for (const entry of before) {
|
|
223
|
+
if (!present.has(entry.component))
|
|
224
|
+
moved.add('geometry');
|
|
225
|
+
}
|
|
226
|
+
return BANDS.filter((band) => moved.has(band));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Every component whose hashes differ, with the bands it differs in.
|
|
230
|
+
*
|
|
231
|
+
* One-sided components contribute `geometry` and nothing else, which is
|
|
232
|
+
* {@link bandsBetween}'s rule and must stay identical to it: a component that is
|
|
233
|
+
* simply not there did not rename anything, and inflating the answer would put
|
|
234
|
+
* an `a11y` claim on a page for a component nobody can look at. `presence` is
|
|
235
|
+
* what carries the rest of that meaning, so no reader has to infer *appeared*
|
|
236
|
+
* from a lone `geometry`.
|
|
237
|
+
*
|
|
238
|
+
* `(unattributed)` is excluded for the reason {@link causesBetween} excludes it:
|
|
239
|
+
* it is a bucket for nodes whose provenance chain broke, so it collects
|
|
240
|
+
* unrelated parts of a page under one name and nothing downstream could act on
|
|
241
|
+
* it. The broken chain is reported where it means something, not here as a
|
|
242
|
+
* component that moved.
|
|
243
|
+
*/
|
|
244
|
+
export function movedBandsBetween(before, after) {
|
|
245
|
+
const previous = new Map(before.map((entry) => [entry.component, entry]));
|
|
246
|
+
const present = new Set(after.map((entry) => entry.component));
|
|
247
|
+
const moved = [];
|
|
248
|
+
for (const entry of after) {
|
|
249
|
+
if (entry.component === UNATTRIBUTED)
|
|
250
|
+
continue;
|
|
251
|
+
const was = previous.get(entry.component);
|
|
252
|
+
if (was === undefined) {
|
|
253
|
+
moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'added' });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const bands = movedBands(was, entry);
|
|
257
|
+
if (bands.length === 0)
|
|
258
|
+
continue;
|
|
259
|
+
const grew = grewBetween(was.boxes, entry.boxes);
|
|
260
|
+
moved.push({
|
|
261
|
+
component: entry.component,
|
|
262
|
+
bands,
|
|
263
|
+
cause: ownContentMoved(was, entry),
|
|
264
|
+
...(grew === undefined ? {} : { grew }),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
for (const entry of before) {
|
|
268
|
+
if (entry.component === UNATTRIBUTED)
|
|
269
|
+
continue;
|
|
270
|
+
if (present.has(entry.component))
|
|
271
|
+
continue;
|
|
272
|
+
moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'removed' });
|
|
273
|
+
}
|
|
274
|
+
return moved.sort((left, right) => left.component.localeCompare(right.component));
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* One size delta both sides agree on, or nothing.
|
|
278
|
+
*
|
|
279
|
+
* Instances are paired by document order, which is the only order either side
|
|
280
|
+
* has. That pairing is sound exactly while the counts match: a component that
|
|
281
|
+
* gained an instance shifted every index after the insertion, and the deltas
|
|
282
|
+
* that fell out would be measurements of one instance against a different one.
|
|
283
|
+
* So a changed count answers nothing rather than answering wrongly — the count
|
|
284
|
+
* itself already moved `structure`, and the reviewer is told that instead.
|
|
285
|
+
*
|
|
286
|
+
* Instances that disagree also answer nothing. Three buttons where one grew and
|
|
287
|
+
* two did not is a real finding and it is not *this* one, and printing the first
|
|
288
|
+
* or the largest would be the page picking a representative and not saying so.
|
|
289
|
+
*
|
|
290
|
+
* Position is deliberately not read. Everything below a control that got taller
|
|
291
|
+
* moved down, and a delta drawn from `x`/`y` would name every one of them.
|
|
292
|
+
*/
|
|
293
|
+
function grewBetween(before, after) {
|
|
294
|
+
if (before === undefined || after === undefined)
|
|
295
|
+
return undefined;
|
|
296
|
+
if (before.length === 0 || before.length !== after.length)
|
|
297
|
+
return undefined;
|
|
298
|
+
let agreed;
|
|
299
|
+
for (const [index, was] of before.entries()) {
|
|
300
|
+
const now = after[index];
|
|
301
|
+
if (was === null || was === undefined || now === null || now === undefined)
|
|
302
|
+
return undefined;
|
|
303
|
+
const width = now.width - was.width;
|
|
304
|
+
const height = now.height - was.height;
|
|
305
|
+
if (agreed === undefined)
|
|
306
|
+
agreed = { width, height };
|
|
307
|
+
else if (agreed.width !== width || agreed.height !== height)
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
return agreed === undefined || (agreed.width === 0 && agreed.height === 0) ? undefined : agreed;
|
|
311
|
+
}
|
|
312
|
+
//# sourceMappingURL=component-hash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-hash.js","sourceRoot":"","sources":["../../src/attribute/component-hash.ts"],"names":[],"mappings":"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","sourcesContent":["import { BANDS, type Band } from '../compare/band.js';\nimport type { CanonicalValue } from '../format/canonical.js';\nimport type { Rect } from '../format/capture.js';\nimport type { Digest } from '../format/hash.js';\nimport { digestValue } from '../format/hash.js';\nimport type { ComponentHash, SemanticSnapshot } from '../format/snapshot.js';\nimport { boundaries, shapeOf, UNATTRIBUTED } from './boundary.js';\n\nexport type { ComponentHash } from '../format/snapshot.js';\nexport { UNATTRIBUTED } from './boundary.js';\n\n/**\n * The digests a comparison of two component states actually reads.\n *\n * `movedBands` used to take two `ComponentHash`es, which carry a name and an\n * instance count neither side of the comparison consults. Narrowing it to the\n * digests is what lets a per-instance record (`ComponentInstance`, which has no\n * `instances` count because it *is* one) be compared by the same function —\n * rather than by a second copy of the band mapping, which is the one thing in\n * this file that must not exist twice: a subject relaxed to `layout` has to\n * absorb the same bands whichever shape the caller happened to be holding.\n */\nexport interface BandDigests {\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n readonly geometry?: Digest;\n}\n\n/**\n * Per-component content hashes, one per band.\n *\n * `SemanticSnapshot` hashes a whole subject. That answers \"did anything change\"\n * and nothing else: it moves whenever anything inside it moves, so it cannot say\n * *which area*, and a record built on it says \"the page changed\" on every commit.\n *\n * These hashes are the unit a history is kept in. Text, and small — a component\n * that did not change contributes the same digest it did last time, so the only\n * thing worth recording is the difference.\n */\n\n/**\n * Hash every component boundary in a subject.\n *\n * Ordered by component name so an unchanged subject produces byte-identical\n * output across runs.\n *\n * The walk and the per-boundary shape live in {@link ./boundary.js}, which\n * {@link ./instances.js} shares — one definition of what a component's own\n * content is, read by the aggregate and by the per-instance form.\n */\nexport function hashComponents(snapshot: SemanticSnapshot): readonly ComponentHash[] {\n const layout = snapshot.profile.layout;\n\n const accumulated = new Map<\n string,\n {\n structure: CanonicalValue[];\n semantics: CanonicalValue[];\n text: CanonicalValue[];\n style: CanonicalValue[];\n geometry: CanonicalValue[];\n boxes: (Rect | null)[];\n }\n >();\n\n for (const boundary of boundaries(snapshot.root)) {\n const shape = shapeOf(boundary, layout);\n\n const entry = accumulated.get(boundary.component) ?? {\n structure: [],\n semantics: [],\n text: [],\n style: [],\n geometry: [],\n boxes: [],\n };\n entry.structure.push(shape.structure);\n entry.semantics.push(shape.semantics);\n entry.text.push(shape.text);\n entry.style.push(shape.style);\n entry.geometry.push(shape.geometry);\n entry.boxes.push(shape.box);\n accumulated.set(boundary.component, entry);\n }\n\n return [...accumulated.entries()]\n .map(([component, entry]) => ({\n component,\n instances: entry.structure.length,\n structure: digestValue(entry.structure),\n semantics: digestValue(entry.semantics),\n text: digestValue(entry.text),\n style: digestValue(entry.style),\n ...(layout ? { geometry: digestValue(entry.geometry) } : {}),\n // Under a profile with no layout every entry is `null`, and a list of\n // nulls is a measurement nobody took written as one they did. Absent\n // instead, which is the same rule `geometry` above obeys.\n ...(layout ? { boxes: entry.boxes } : {}),\n }))\n // Code-unit order, not `localeCompare`. The doc above promises byte-identical\n // output for an unchanged subject, and a locale-aware comparison makes that a\n // promise about the machine's `LANG` — which was tolerable while these were\n // internal and is not now that they are written into a sidecar, committed\n // beside a baseline, and read back on someone else's runner.\n .sort((a, b) => (a.component < b.component ? -1 : a.component > b.component ? 1 : 0));\n}\n\n/**\n * Which components *caused* a change, given both revisions' hashes.\n *\n * The arithmetic behind cause-first ranking, and the reason a baseline carries\n * its component hashes at all. `rankRegions` takes a list of cause names and,\n * given none, falls back to area — which\n * [journal 0013](../../../../docs/context/journal/0013-observability.md)\n * measured as backwards by 6×, because area measures displacement rather than\n * cause. This is where that list comes from on a path with no second document.\n *\n * The distinction is the split ADR-0018 built the bands for:\n *\n * - **`structure` or `style` moved** — the component's own content is different.\n * It is a cause.\n * - **only `geometry` moved** — the component is byte-identical and its box is\n * somewhere else. Something *else* moved it, so it is collateral, and naming\n * it would send a reviewer to a file nobody edited.\n * - **appeared or disappeared** — a component present on one side only is a\n * cause. Something decided to render it or to stop.\n *\n * `instances` is deliberately not consulted. A component rendered five times\n * instead of four has a different `structure` digest for the subject, and the\n * count moving on its own — the same component, the same content, one more of\n * them — is a change in whatever decided how many, not in this component.\n *\n * A profile with no layout supplies no `geometry`, so on that tier every\n * difference is `structure` or `style` and every changed component is a cause.\n * That is correct rather than degraded: with no boxes, nothing was displaced.\n */\nexport function causesBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly string[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const causes: string[] = [];\n\n for (const entry of after) {\n // `(unattributed)` is not a component and can never be a cause. It is the\n // bucket for nodes whose provenance chain broke, so it collects unrelated\n // parts of a page under one name — and nothing downstream could act on it\n // anyway: a region with no owner reports no component, so it would never\n // match. A broken chain is a defect in this tool and is reported as\n // `unattributed` where that means something, not smuggled in here as a\n // culprit.\n if (entry.component === UNATTRIBUTED) continue;\n\n const was = previous.get(entry.component);\n if (was === undefined) {\n causes.push(entry.component);\n continue;\n }\n if (ownContentMoved(was, entry)) causes.push(entry.component);\n }\n\n // Removals too, and they are the case a candidate-only walk cannot see: a\n // component that stopped rendering leaves regions behind it, and the component\n // that used to be there is exactly the name a reviewer needs.\n const present = new Set(after.map((entry) => entry.component));\n for (const entry of before) {\n if (entry.component === UNATTRIBUTED) continue;\n if (!present.has(entry.component)) causes.push(entry.component);\n }\n\n return causes.sort();\n}\n\n/**\n * Whether a component's *own content* differs, ignoring where its box ended up.\n *\n * Deliberately not phrased in bands, because it is not a band question. Both\n * `structure` and `geometry` map to the `geometry` band, and this has to keep\n * them apart: a component whose tree changed edited itself, and a component\n * whose rect moved was pushed. That distinction is the entire cause/collateral\n * result, and asking it through the band mapping would need the digests back\n * again to answer it.\n *\n * The list is every digest a component owns except `geometry`. It grew by two on\n * 2026-08-06 without changing meaning: `semantics` and `text` used to be inside\n * `structure`.\n */\nfunction ownContentMoved(before: BandDigests, after: BandDigests): boolean {\n return (\n before.structure !== after.structure ||\n before.semantics !== after.semantics ||\n before.text !== after.text ||\n before.style !== after.style\n );\n}\n\n/**\n * Which frequency bands moved between one component's two hashes.\n *\n * The reason the digests were split. A baseline carries hashes and not\n * documents, so \"what changed here\" used to be answerable only as a boolean —\n * and a boolean cannot serve a route-level test, whose entire request is *tell\n * me when the page stops assembling and never when it is repainted*.\n *\n * The mapping is exact and it is the same one `bandOf` applies to a delta, which\n * is the property that matters: a subject relaxed to `layout` must absorb the\n * same things whether the run held two documents or two sidecars. Two mappings\n * would be one drift away from a config key meaning different things on the two\n * paths, discovered as a regression somebody let through.\n *\n * | digest | band | what it covers |\n * |---|---|---|\n * | `semantics` | `a11y` | role, accessible name, ARIA state |\n * | `text` | `content` | text runs |\n * | `structure` | `geometry` | tags, aliases, attributes, child boundaries |\n * | `geometry` | `geometry` | rects and computed layout output |\n * | `style` | `token` | declared values and custom properties |\n *\n * `texture` never appears. It is raster residue by definition, and a component\n * hash is built from a document — so the band a comparison of hashes cannot\n * decide is *absent* from the answer rather than reported as unmoved, which is\n * ADR-0002's rule applied to a narrower question.\n *\n * A missing `geometry` on either side is the profile saying it has no layout\n * engine, and is not a difference. Treating absent as a change would report\n * every component as having moved the moment a jsdom baseline met a Chromium\n * run — which the environment key already refuses as `incomparable`, so this\n * would be a second, wronger answer to a question already settled.\n */\nexport function movedBands(before: BandDigests, after: BandDigests): readonly Band[] {\n const moved = new Set<Band>();\n\n if (before.semantics !== after.semantics) moved.add('a11y');\n if (before.text !== after.text) moved.add('content');\n if (before.structure !== after.structure) moved.add('geometry');\n if (before.style !== after.style) moved.add('token');\n if (\n before.geometry !== undefined &&\n after.geometry !== undefined &&\n before.geometry !== after.geometry\n ) {\n moved.add('geometry');\n }\n\n return BANDS.filter((band) => moved.has(band));\n}\n\n/**\n * Every band that moved anywhere in the subject, given both revisions' hashes.\n *\n * A component present on one side only contributes `geometry`: something was\n * added or removed, which is the structural half of that band however the rest\n * of it compares. It deliberately does not contribute `a11y` or `content` as\n * well — a component that is simply not there did not *rename* anything, and\n * inflating the answer would make a level that absorbs nothing look like the\n * only safe choice.\n */\nexport function bandsBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly Band[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const present = new Set(after.map((entry) => entry.component));\n const moved = new Set<Band>();\n\n for (const entry of after) {\n const was = previous.get(entry.component);\n if (was === undefined) moved.add('geometry');\n else for (const band of movedBands(was, entry)) moved.add(band);\n }\n\n for (const entry of before) {\n if (!present.has(entry.component)) moved.add('geometry');\n }\n\n return BANDS.filter((band) => moved.has(band));\n}\n\n/**\n * Which bands moved, kept per component instead of folded into one list.\n *\n * {@link bandsBetween} answers *what kind of change is in this subject*, which is\n * the question a sensitivity level asks. It cannot answer the one a reviewer\n * asks — *what changed, and where* — because the fold is lossy in exactly the\n * place attribution lives: a subject reporting `content, geometry, token` has\n * told you a colour and a string and a size all moved somewhere in it, and left\n * you to guess which of the forty components on the page owns which.\n *\n * Unfolded, the same two sidecars say `Button — geometry, token` and\n * `CardFooter — a11y, content`, and that is a sentence a page can print beside a\n * picture. It is also the record that survives when the raster tier loses the\n * name: a difference that reflowed its neighbours merges into one blob whose box\n * fits no component, so the region resolves to the document root and the edit\n * arrives unattributed — while the hashes, which never looked at a pixel, still\n * hold the component that moved and the sense in which it moved.\n *\n * `cause` repeats the {@link causesBetween} predicate rather than being derived\n * from `bands`, and the repetition is the point: `structure` and `geometry` both\n * map to the `geometry` band, so a component that edited its own tree and one\n * that was merely pushed by a neighbour are indistinguishable *after* the band\n * mapping. Losing that here would make every reflowed container a culprit.\n */\nexport interface ComponentBands {\n readonly component: string;\n /** Non-empty: a component whose digests all matched has no entry at all. */\n readonly bands: readonly Band[];\n /** Its own content moved, as opposed to only its rect. `causesBetween`'s test. */\n readonly cause: boolean;\n /** Set only when the component is on one side alone, which `bands` cannot say. */\n readonly presence?: 'added' | 'removed';\n\n /**\n * How much bigger its own box got, when every instance agrees on the answer.\n *\n * The band a reviewer actually wanted. `geometry` says a rect under here is\n * not the rect it was; this says the control is eight pixels taller and\n * thirty-six wider, which is the padding somebody edited, arriving as a\n * measurement rather than as a guess about which property produced it.\n *\n * Absent for four different reasons and they are one reason: nobody can say.\n * No boxes on one side, a different number of instances, instances that\n * disagree about the delta, or a box that did not change size. Present is\n * always non-zero on at least one axis.\n */\n readonly grew?: { readonly width: number; readonly height: number };\n}\n\n/**\n * Every component whose hashes differ, with the bands it differs in.\n *\n * One-sided components contribute `geometry` and nothing else, which is\n * {@link bandsBetween}'s rule and must stay identical to it: a component that is\n * simply not there did not rename anything, and inflating the answer would put\n * an `a11y` claim on a page for a component nobody can look at. `presence` is\n * what carries the rest of that meaning, so no reader has to infer *appeared*\n * from a lone `geometry`.\n *\n * `(unattributed)` is excluded for the reason {@link causesBetween} excludes it:\n * it is a bucket for nodes whose provenance chain broke, so it collects\n * unrelated parts of a page under one name and nothing downstream could act on\n * it. The broken chain is reported where it means something, not here as a\n * component that moved.\n */\nexport function movedBandsBetween(\n before: readonly ComponentHash[],\n after: readonly ComponentHash[],\n): readonly ComponentBands[] {\n const previous = new Map(before.map((entry) => [entry.component, entry]));\n const present = new Set(after.map((entry) => entry.component));\n const moved: ComponentBands[] = [];\n\n for (const entry of after) {\n if (entry.component === UNATTRIBUTED) continue;\n const was = previous.get(entry.component);\n if (was === undefined) {\n moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'added' });\n continue;\n }\n const bands = movedBands(was, entry);\n if (bands.length === 0) continue;\n const grew = grewBetween(was.boxes, entry.boxes);\n moved.push({\n component: entry.component,\n bands,\n cause: ownContentMoved(was, entry),\n ...(grew === undefined ? {} : { grew }),\n });\n }\n\n for (const entry of before) {\n if (entry.component === UNATTRIBUTED) continue;\n if (present.has(entry.component)) continue;\n moved.push({ component: entry.component, bands: ['geometry'], cause: true, presence: 'removed' });\n }\n\n return moved.sort((left, right) => left.component.localeCompare(right.component));\n}\n\n/**\n * One size delta both sides agree on, or nothing.\n *\n * Instances are paired by document order, which is the only order either side\n * has. That pairing is sound exactly while the counts match: a component that\n * gained an instance shifted every index after the insertion, and the deltas\n * that fell out would be measurements of one instance against a different one.\n * So a changed count answers nothing rather than answering wrongly — the count\n * itself already moved `structure`, and the reviewer is told that instead.\n *\n * Instances that disagree also answer nothing. Three buttons where one grew and\n * two did not is a real finding and it is not *this* one, and printing the first\n * or the largest would be the page picking a representative and not saying so.\n *\n * Position is deliberately not read. Everything below a control that got taller\n * moved down, and a delta drawn from `x`/`y` would name every one of them.\n */\nfunction grewBetween(\n before: readonly (Rect | null)[] | undefined,\n after: readonly (Rect | null)[] | undefined,\n): { readonly width: number; readonly height: number } | undefined {\n if (before === undefined || after === undefined) return undefined;\n if (before.length === 0 || before.length !== after.length) return undefined;\n\n let agreed: { width: number; height: number } | undefined;\n for (const [index, was] of before.entries()) {\n const now = after[index];\n if (was === null || was === undefined || now === null || now === undefined) return undefined;\n\n const width = now.width - was.width;\n const height = now.height - was.height;\n if (agreed === undefined) agreed = { width, height };\n else if (agreed.width !== width || agreed.height !== height) return undefined;\n }\n\n return agreed === undefined || (agreed.width === 0 && agreed.height === 0) ? undefined : agreed;\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One small suite, built twice over: as a graph, and as something that moved.
|
|
3
|
+
*
|
|
4
|
+
* `composition.test.ts` proves the fold and `movement.test.ts` proves the ladder
|
|
5
|
+
* that reads it, and they need the same three subjects to do it. A chip in its
|
|
6
|
+
* own story and the same chip in a page is the smallest arrangement in which an
|
|
7
|
+
* echo, a divergence and a control group are all available at once, so it is
|
|
8
|
+
* described once here rather than diverging in two files.
|
|
9
|
+
*/
|
|
10
|
+
import type { ComponentInstance } from './instances.js';
|
|
11
|
+
import type { SubjectComposition } from './composition.js';
|
|
12
|
+
export declare function instance(over: Partial<ComponentInstance> & {
|
|
13
|
+
component: string;
|
|
14
|
+
}): ComponentInstance;
|
|
15
|
+
/** A chip, rendered the same way, with the same inputs. */
|
|
16
|
+
export declare const chip: (path: string, within: string, over?: Partial<ComponentInstance>) => ComponentInstance;
|
|
17
|
+
export declare const SUITE: readonly SubjectComposition[];
|
|
18
|
+
//# sourceMappingURL=composition-fixture.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One small suite, built twice over: as a graph, and as something that moved.
|
|
3
|
+
*
|
|
4
|
+
* `composition.test.ts` proves the fold and `movement.test.ts` proves the ladder
|
|
5
|
+
* that reads it, and they need the same three subjects to do it. A chip in its
|
|
6
|
+
* own story and the same chip in a page is the smallest arrangement in which an
|
|
7
|
+
* echo, a divergence and a control group are all available at once, so it is
|
|
8
|
+
* described once here rather than diverging in two files.
|
|
9
|
+
*/
|
|
10
|
+
export function instance(over) {
|
|
11
|
+
return {
|
|
12
|
+
path: '0',
|
|
13
|
+
depth: 1,
|
|
14
|
+
rendering: 'v1:r',
|
|
15
|
+
structure: 'v1:s',
|
|
16
|
+
semantics: 'v1:a',
|
|
17
|
+
text: 'v1:t',
|
|
18
|
+
style: 'v1:y',
|
|
19
|
+
renders: [],
|
|
20
|
+
nodes: 1,
|
|
21
|
+
tokens: [],
|
|
22
|
+
...over,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** A chip, rendered the same way, with the same inputs. */
|
|
26
|
+
export const chip = (path, within, over = {}) => instance({
|
|
27
|
+
component: 'Chip',
|
|
28
|
+
path,
|
|
29
|
+
within,
|
|
30
|
+
depth: 2,
|
|
31
|
+
props: 'v1:chip',
|
|
32
|
+
rendering: 'v1:chip-done',
|
|
33
|
+
...over,
|
|
34
|
+
});
|
|
35
|
+
export const SUITE = [
|
|
36
|
+
{
|
|
37
|
+
subject: 'story:ds-chip--done',
|
|
38
|
+
instances: [
|
|
39
|
+
instance({ component: 'Story', path: '0', depth: 0, renders: ['Chip'] }),
|
|
40
|
+
chip('0/0', 'Story'),
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
subject: 'story:page--default',
|
|
45
|
+
instances: [
|
|
46
|
+
instance({ component: 'App', path: '0', depth: 0, renders: ['Footer'] }),
|
|
47
|
+
instance({ component: 'Footer', path: '0/1', depth: 1, renders: ['Chip', 'Chip'] }),
|
|
48
|
+
chip('0/1/0', 'Footer', { props: 'v1:chip-all', rendering: 'v1:chip-all' }),
|
|
49
|
+
chip('0/1/1', 'Footer'),
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
//# sourceMappingURL=composition-fixture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition-fixture.js","sourceRoot":"","sources":["../../src/attribute/composition-fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,MAAM,UAAU,QAAQ,CAAC,IAAwD;IAC/E,OAAO;QACL,IAAI,EAAE,GAAG;QACT,KAAK,EAAE,CAAC;QACR,SAAS,EAAE,MAAM;QACjB,SAAS,EAAE,MAAM;QACjB,SAAS,EAAE,MAAM;QACjB,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,MAAM;QACb,OAAO,EAAE,EAAE;QACX,KAAK,EAAE,CAAC;QACR,MAAM,EAAE,EAAE;QACV,GAAG,IAAI;KACR,CAAC;AACJ,CAAC;AAED,2DAA2D;AAC3D,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,MAAc,EAAE,IAAI,GAA+B,EAAE,EAAE,EAAE,CAC1F,QAAQ,CAAC;IACP,SAAS,EAAE,MAAM;IACjB,IAAI;IACJ,MAAM;IACN,KAAK,EAAE,CAAC;IACR,KAAK,EAAE,SAAS;IAChB,SAAS,EAAE,cAAc;IACzB,GAAG,IAAI;CACR,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,KAAK,GAAkC;IAClD;QACE,OAAO,EAAE,qBAAqB;QAC9B,SAAS,EAAE;YACT,QAAQ,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;YACxE,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC;SACrB;KACF;IACD;QACE,OAAO,EAAE,qBAAqB;QAC9B,SAAS,EAAE;YACT,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxE,QAAQ,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;YACnF,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC;YAC3E,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC;SACxB;KACF;CACF,CAAC","sourcesContent":["/**\n * One small suite, built twice over: as a graph, and as something that moved.\n *\n * `composition.test.ts` proves the fold and `movement.test.ts` proves the ladder\n * that reads it, and they need the same three subjects to do it. A chip in its\n * own story and the same chip in a page is the smallest arrangement in which an\n * echo, a divergence and a control group are all available at once, so it is\n * described once here rather than diverging in two files.\n */\n\nimport type { ComponentInstance } from './instances.js';\nimport type { SubjectComposition } from './composition.js';\n\nexport function instance(over: Partial<ComponentInstance> & { component: string }): ComponentInstance {\n return {\n path: '0',\n depth: 1,\n rendering: 'v1:r',\n structure: 'v1:s',\n semantics: 'v1:a',\n text: 'v1:t',\n style: 'v1:y',\n renders: [],\n nodes: 1,\n tokens: [],\n ...over,\n };\n}\n\n/** A chip, rendered the same way, with the same inputs. */\nexport const chip = (path: string, within: string, over: Partial<ComponentInstance> = {}) =>\n instance({\n component: 'Chip',\n path,\n within,\n depth: 2,\n props: 'v1:chip',\n rendering: 'v1:chip-done',\n ...over,\n });\n\nexport const SUITE: readonly SubjectComposition[] = [\n {\n subject: 'story:ds-chip--done',\n instances: [\n instance({ component: 'Story', path: '0', depth: 0, renders: ['Chip'] }),\n chip('0/0', 'Story'),\n ],\n },\n {\n subject: 'story:page--default',\n instances: [\n instance({ component: 'App', path: '0', depth: 0, renders: ['Footer'] }),\n instance({ component: 'Footer', path: '0/1', depth: 1, renders: ['Chip', 'Chip'] }),\n chip('0/1/0', 'Footer', { props: 'v1:chip-all', rendering: 'v1:chip-all' }),\n chip('0/1/1', 'Footer'),\n ],\n },\n];\n"]}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { Digest } from '../format/hash.js';
|
|
2
|
+
import type { NodePath, SemanticSnapshot } from '../format/snapshot.js';
|
|
3
|
+
import { type Divergence } from './divergence.js';
|
|
4
|
+
import { type ComponentInstance } from './instances.js';
|
|
5
|
+
/**
|
|
6
|
+
* The suite read as one graph, at one commit.
|
|
7
|
+
*
|
|
8
|
+
* Every other comparison in this system is between two revisions of one subject.
|
|
9
|
+
* This is the other axis: **many subjects, one revision**, joined on the
|
|
10
|
+
* components they share. It answers the question a per-subject tool cannot even
|
|
11
|
+
* phrase — *this component, here, is the same component you already have an
|
|
12
|
+
* example of over there* — and it answers it from digests a run already
|
|
13
|
+
* computed, with no second render, no image and no store.
|
|
14
|
+
*
|
|
15
|
+
* ## What the join is worth
|
|
16
|
+
*
|
|
17
|
+
* A suite of examples is a suite of *compositions*. The narrow example is a
|
|
18
|
+
* component at a boundary; the page examples contain that same boundary again,
|
|
19
|
+
* sometimes with the same props and sometimes not. Once the instances are
|
|
20
|
+
* addressable, three facts fall out that nobody had:
|
|
21
|
+
*
|
|
22
|
+
* - **An echo.** One rendering, several subjects. The narrow example and the
|
|
23
|
+
* page are watching literally the same bytes, so a change in one is a change
|
|
24
|
+
* in all of them, and a reviewer looking at eleven diffs is looking at one.
|
|
25
|
+
* - **A divergence.** Same component, same props, *different* rendering, at one
|
|
26
|
+
* commit. Nothing about the component's own inputs can explain that. Either
|
|
27
|
+
* something outside it decides its output — an ancestor's cascade, a token, its
|
|
28
|
+
* own state — or the reading is not repeatable. Both are findings and neither
|
|
29
|
+
* is a regression.
|
|
30
|
+
* - **A control group.** For any instance that moved, the same rendering
|
|
31
|
+
* somewhere else that did *not* is a stable state to refer to. That is the
|
|
32
|
+
* referent flake attribution has always needed and never had, and the suite
|
|
33
|
+
* supplies it for free: one commit already holds N renderings of every shared
|
|
34
|
+
* component.
|
|
35
|
+
*
|
|
36
|
+
* ## What it deliberately does not do
|
|
37
|
+
*
|
|
38
|
+
* It never decides anything. Two subjects sharing a rendering is not a reason to
|
|
39
|
+
* delete either — that is the argument `coverage.ts` makes at the level of names
|
|
40
|
+
* and it holds harder here, because a component can be correct in one context
|
|
41
|
+
* and broken in the next, which is why the contexts are separate subjects.
|
|
42
|
+
*/
|
|
43
|
+
/** One instance, located. */
|
|
44
|
+
export interface Site {
|
|
45
|
+
readonly subject: string;
|
|
46
|
+
readonly path: NodePath;
|
|
47
|
+
readonly depth: number;
|
|
48
|
+
readonly within?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The component whose JSX wrote this element, here.
|
|
51
|
+
*
|
|
52
|
+
* On the entry beside it, `createdBy` is a set folded over the whole suite —
|
|
53
|
+
* every component that ever wrote one of these, anywhere. That is the right
|
|
54
|
+
* shape for *who mounts this* and the wrong one for *who mounted this here*,
|
|
55
|
+
* and the ladder asks the second: an edit to a component that writes a `Chip`
|
|
56
|
+
* on the footer explains nothing about a `Chip` the sidebar wrote. Kept per
|
|
57
|
+
* site so the question can be asked where it was answered.
|
|
58
|
+
*/
|
|
59
|
+
readonly createdBy?: string;
|
|
60
|
+
}
|
|
61
|
+
/** One rendering of one component, and everywhere it occurred. */
|
|
62
|
+
export interface Rendering {
|
|
63
|
+
readonly rendering: Digest;
|
|
64
|
+
readonly structure: Digest;
|
|
65
|
+
readonly semantics: Digest;
|
|
66
|
+
readonly text: Digest;
|
|
67
|
+
readonly style: Digest;
|
|
68
|
+
/**
|
|
69
|
+
* Child components this rendering mounted, in document order.
|
|
70
|
+
*
|
|
71
|
+
* Carried on the rendering rather than only on the entry because it is the
|
|
72
|
+
* only evidence available about what was passed *in*: `propsDigest` excludes
|
|
73
|
+
* `children` by design, so two renderings that mount different children are
|
|
74
|
+
* two different inputs wearing one props digest. `divergencesOf` refuses to
|
|
75
|
+
* call that a contradiction, and this is the field it reads.
|
|
76
|
+
*/
|
|
77
|
+
readonly renders: readonly string[];
|
|
78
|
+
/** In subject order, then document order. */
|
|
79
|
+
readonly sites: readonly Site[];
|
|
80
|
+
}
|
|
81
|
+
/** One props digest of one component, and every way it rendered under it. */
|
|
82
|
+
export interface PropsClass {
|
|
83
|
+
/** Absent when the collector supplied no provenance — unknown, not "none". */
|
|
84
|
+
readonly props?: Digest;
|
|
85
|
+
readonly renderings: readonly Rendering[];
|
|
86
|
+
}
|
|
87
|
+
export interface ComponentEntry {
|
|
88
|
+
readonly component: string;
|
|
89
|
+
/** Subjects containing at least one boundary of it, in the order supplied. */
|
|
90
|
+
readonly subjects: readonly string[];
|
|
91
|
+
/** Boundaries summed across every subject. Distinct from `subjects.length`. */
|
|
92
|
+
readonly instances: number;
|
|
93
|
+
/**
|
|
94
|
+
* Subjects whose shallowest attributed boundary is this component.
|
|
95
|
+
*
|
|
96
|
+
* The narrow example — the subject that exists to show this thing, rather than
|
|
97
|
+
* a page that happens to contain it. Defined by depth rather than by a naming
|
|
98
|
+
* convention because a naming convention is a different tool's `story:` prefix,
|
|
99
|
+
* and this has to hold for a route suite too. Ties resolve to nothing: a
|
|
100
|
+
* subject whose two shallowest boundaries are siblings has no single subject.
|
|
101
|
+
*/
|
|
102
|
+
readonly examples: readonly string[];
|
|
103
|
+
/** Components that enclose it somewhere, sorted. The graph, upwards. */
|
|
104
|
+
readonly within: readonly string[];
|
|
105
|
+
/**
|
|
106
|
+
* Components that *mounted* it somewhere, sorted. The graph, upwards, again —
|
|
107
|
+
* and usually the more useful of the two.
|
|
108
|
+
*
|
|
109
|
+
* `within` is where the boundary sits in the DOM and this is who wrote the
|
|
110
|
+
* element. They are the same thing only for a component that authors a node of
|
|
111
|
+
* its own, and a real application is full of components that do not: measured
|
|
112
|
+
* on `examples/todomvc`, every `Chip` reports `within: ['Stack']` and
|
|
113
|
+
* `createdBy: ['TodoFooter']`. `TodoFooter` renders nothing but other
|
|
114
|
+
* components, so it owns no node, is a boundary nowhere, and appears in this
|
|
115
|
+
* graph *only* here — while being the file a reviewer has to open.
|
|
116
|
+
*
|
|
117
|
+
* Empty on a production build, where `_debugOwner` is absent (ADR-0007). Empty
|
|
118
|
+
* is *not* "nothing mounted it": a component with no caller is the subject root
|
|
119
|
+
* and every other case is missing data, which is why the `upstream` rung in
|
|
120
|
+
* `movement.ts` reads this and never concludes from its silence.
|
|
121
|
+
*/
|
|
122
|
+
readonly createdBy: readonly string[];
|
|
123
|
+
/** Components it encloses somewhere, sorted. The graph, downwards. */
|
|
124
|
+
readonly renders: readonly string[];
|
|
125
|
+
/**
|
|
126
|
+
* Custom properties its own nodes resolve through, anywhere in the suite.
|
|
127
|
+
*
|
|
128
|
+
* The set a token movement is intersected against, which is how "`--va-space-3`
|
|
129
|
+
* moved" becomes "`--va-space-3` moved and these four components read it".
|
|
130
|
+
*/
|
|
131
|
+
readonly tokens: readonly string[];
|
|
132
|
+
/** Sorted by props digest; the class with unknown props sorts last. */
|
|
133
|
+
readonly classes: readonly PropsClass[];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The same component, the same props, the same bytes, in more than one subject.
|
|
137
|
+
*
|
|
138
|
+
* The connected dots. `example` names the narrow subject among the sites when
|
|
139
|
+
* there is one, because that is the artefact a reviewer already has an opinion
|
|
140
|
+
* about — "this is the Chip from the Chip story" is a sentence, and "these four
|
|
141
|
+
* paths share a digest" is not.
|
|
142
|
+
*/
|
|
143
|
+
export interface Echo {
|
|
144
|
+
readonly component: string;
|
|
145
|
+
readonly props?: Digest;
|
|
146
|
+
readonly rendering: Digest;
|
|
147
|
+
/** At least two, spanning at least two subjects. */
|
|
148
|
+
readonly sites: readonly Site[];
|
|
149
|
+
readonly example?: string;
|
|
150
|
+
}
|
|
151
|
+
export interface SubjectComposition {
|
|
152
|
+
readonly subject: string;
|
|
153
|
+
readonly instances: readonly ComponentInstance[];
|
|
154
|
+
/**
|
|
155
|
+
* The subject as read, kept so a divergence can say *which input* moved.
|
|
156
|
+
*
|
|
157
|
+
* Optional because `composeSubjects` is a fold over instance lists and stays
|
|
158
|
+
* one — a caller holding only a report's sidecars still gets the graph, the
|
|
159
|
+
* echoes and the divergences, and gets them without a `partings` field it
|
|
160
|
+
* would have to explain away as empty.
|
|
161
|
+
*/
|
|
162
|
+
readonly snapshot?: SemanticSnapshot;
|
|
163
|
+
}
|
|
164
|
+
export interface Composition {
|
|
165
|
+
/** In the order supplied, which the run guarantees is plan order. */
|
|
166
|
+
readonly subjects: readonly string[];
|
|
167
|
+
/** Sorted by name, code-unit order. */
|
|
168
|
+
readonly components: readonly ComponentEntry[];
|
|
169
|
+
/** Sorted by how many sites each has, widest first, then by component. */
|
|
170
|
+
readonly echoes: readonly Echo[];
|
|
171
|
+
/** Sorted by component name. */
|
|
172
|
+
readonly divergences: readonly Divergence[];
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Fold a run's per-subject instance lists into the graph.
|
|
176
|
+
*
|
|
177
|
+
* Pure, ordered, and a function of its input alone — the run's report has to be
|
|
178
|
+
* a function of the plan, and a phase that accumulated as a worker pool finished
|
|
179
|
+
* would produce a different artefact from the same suite on a slower machine.
|
|
180
|
+
*/
|
|
181
|
+
export declare function composeSubjects(subjects: readonly SubjectComposition[]): Composition;
|
|
182
|
+
//# sourceMappingURL=composition.d.ts.map
|