@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,179 @@
|
|
|
1
|
+
import type { Digest } from '../format/hash.js';
|
|
2
|
+
import type { NodePath, SemanticSnapshot } from '../format/snapshot.js';
|
|
3
|
+
import { type Band } from './band.js';
|
|
4
|
+
import { type Delta } from './diff/index.js';
|
|
5
|
+
import { type PartingSlice } from './slice.js';
|
|
6
|
+
/**
|
|
7
|
+
* Where two readings of one page parted.
|
|
8
|
+
*
|
|
9
|
+
* A diff says a `<div>` rendered a `<p>` on one side and a `<span>` on the
|
|
10
|
+
* other. That is a symptom, and acting on it means reading the component,
|
|
11
|
+
* guessing which branch ran, and guessing why. This says: `Cart`'s `useState` at
|
|
12
|
+
* call 2 held different values, so `Summary` was handed a different `total`, so
|
|
13
|
+
* the tag moved. Same evidence, three rungs up.
|
|
14
|
+
*
|
|
15
|
+
* The rung it can reach is decided by how much of the boundary was readable, and
|
|
16
|
+
* the difference between the top two rungs is the whole point:
|
|
17
|
+
*
|
|
18
|
+
* - **collateral** — a prop or a context this boundary *received* moved, so the
|
|
19
|
+
* difference arrived from outside and something above it is the cause.
|
|
20
|
+
* - **origin** — every input this boundary received held, and its own retained
|
|
21
|
+
* state moved. Nothing above it explains this; it is the answer.
|
|
22
|
+
*
|
|
23
|
+
* The mechanism is one rule applied at every boundary: *a component whose
|
|
24
|
+
* inputs agreed and whose output moved decided differently.* Walking the tree
|
|
25
|
+
* for the shallowest boundary where that holds is what turns a page of deltas
|
|
26
|
+
* into one sentence — and the same rule, run over two readings of one page
|
|
27
|
+
* rather than two arms of an experiment, is what separates a flake from a
|
|
28
|
+
* change. A flake is the case where nothing readable moved at all
|
|
29
|
+
* ({@link PartingRung} `undetermined`), and it is only nameable because
|
|
30
|
+
* `unread` is a rung of its own: an accusation of nondeterminism may only be
|
|
31
|
+
* made about inputs somebody actually read (ADR-0002).
|
|
32
|
+
*
|
|
33
|
+
* Not `attribute/composition.ts`'s `Divergence`, which is the same suspicion
|
|
34
|
+
* arrived at from the other side: one component rendering two ways from one
|
|
35
|
+
* props digest *within* a run, found by counting sites. That one can say a
|
|
36
|
+
* component's inputs do not determine its output; this one can say which input.
|
|
37
|
+
*
|
|
38
|
+
* Nothing here reaches a hash. {@link Holding} is evidence carried beside the
|
|
39
|
+
* snapshot, exactly as `styleProvenance` is, and for the reason `wiring.ts`
|
|
40
|
+
* gives: a hook's value is the thing that legitimately differs between two
|
|
41
|
+
* readings of one page, so a band carrying it would be a flake generator wearing
|
|
42
|
+
* a band's name. This module is the reader that value was kept for.
|
|
43
|
+
*/
|
|
44
|
+
export interface Parting {
|
|
45
|
+
/**
|
|
46
|
+
* What kind of parting this is, decided before which input moved.
|
|
47
|
+
*
|
|
48
|
+
* The triage line. `refactor` and `settled` are pages nobody needs to open,
|
|
49
|
+
* `flake` is a page whose baseline is the problem, and `variation` is the
|
|
50
|
+
* only one where the rungs below are worth reading.
|
|
51
|
+
*/
|
|
52
|
+
readonly slice: PartingSlice;
|
|
53
|
+
/** `true` when both render hashes agree. Inputs may still have moved. */
|
|
54
|
+
readonly identical: boolean;
|
|
55
|
+
/** Every delta, unchanged, so a caller need not run the comparison twice. */
|
|
56
|
+
readonly deltas: readonly Delta[];
|
|
57
|
+
/**
|
|
58
|
+
* Boundaries where something moved — an input, an output, or both — in
|
|
59
|
+
* document order.
|
|
60
|
+
*
|
|
61
|
+
* Absent, never `[]`, when no node on either side started a component: a page
|
|
62
|
+
* read with no framework adapter has not been found to agree, it has not been
|
|
63
|
+
* asked (ADR-0002). Present with every boundary at rung `unread` is the next
|
|
64
|
+
* reading up — the components were found and their inputs were not.
|
|
65
|
+
*/
|
|
66
|
+
readonly boundaries?: readonly PartedBoundary[];
|
|
67
|
+
/**
|
|
68
|
+
* The boundaries the difference started at, shallowest first.
|
|
69
|
+
*
|
|
70
|
+
* A subset of {@link boundaries}: those whose output moved without an
|
|
71
|
+
* incoming input to explain it. Usually one. More than one means the two
|
|
72
|
+
* renders differ in more than one place, which is a real answer rather than a
|
|
73
|
+
* failure to narrow.
|
|
74
|
+
*/
|
|
75
|
+
readonly origins?: readonly PartedBoundary[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* One component boundary, and what moved at it.
|
|
79
|
+
*
|
|
80
|
+
* A boundary is a component's root host node — `componentFiberOf`'s definition,
|
|
81
|
+
* shared with the wiring band so the two can never disagree about where a
|
|
82
|
+
* component starts. A component that vanished entirely has no boundary here: its
|
|
83
|
+
* nodes leave `node-removed` deltas that attribute to the *parent* boundary,
|
|
84
|
+
* which is the correct answer, because deciding not to render a child is
|
|
85
|
+
* something the parent did.
|
|
86
|
+
*/
|
|
87
|
+
export interface PartedBoundary {
|
|
88
|
+
/** `displayName`, or `(anonymous)` when provenance did not name it. */
|
|
89
|
+
readonly component: string;
|
|
90
|
+
/** Path in the candidate tree. */
|
|
91
|
+
readonly path: NodePath;
|
|
92
|
+
/** Depth of {@link path}, so callers can order without re-parsing it. */
|
|
93
|
+
readonly depth: number;
|
|
94
|
+
readonly rung: PartingRung;
|
|
95
|
+
/**
|
|
96
|
+
* Inputs that differ: props, then contexts, then hook calls in order, then
|
|
97
|
+
* the inherited properties no declaration at this boundary accounts for.
|
|
98
|
+
*/
|
|
99
|
+
readonly inputs: readonly MovedInput[];
|
|
100
|
+
/**
|
|
101
|
+
* Deltas at or under this boundary that no nested boundary owns.
|
|
102
|
+
*
|
|
103
|
+
* Zero is meaningful and is not a filtered-out row: a boundary with moved
|
|
104
|
+
* inputs and no deltas of its own is the negative result an experiment
|
|
105
|
+
* usually wants — this arm was assigned differently and rendered the same.
|
|
106
|
+
*/
|
|
107
|
+
readonly deltas: number;
|
|
108
|
+
/** Bands those deltas fall in, in band order. */
|
|
109
|
+
readonly bands: readonly Band[];
|
|
110
|
+
/**
|
|
111
|
+
* The properties the owned deltas named, deduplicated and in code-unit order.
|
|
112
|
+
*
|
|
113
|
+
* The last joint of the chain the rungs above climb: a hook cell moved, a prop
|
|
114
|
+
* carried it down, and *this* is what the prop turned into on the page —
|
|
115
|
+
* `color`, `padding-top`, `width`. A reader chasing a visual regression is
|
|
116
|
+
* looking for this list, and `deltas: 7` alone sends them back to the delta
|
|
117
|
+
* array to assemble it.
|
|
118
|
+
*
|
|
119
|
+
* Absent when no owned delta named a property, which is a reading rather than
|
|
120
|
+
* a gap: a removed node and a changed accessible name are whole-node facts and
|
|
121
|
+
* have no property to name. `deltas` and `bands` still say what happened
|
|
122
|
+
* there.
|
|
123
|
+
*/
|
|
124
|
+
readonly moved?: readonly string[];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* How far up the difference could be traced at one boundary.
|
|
128
|
+
*
|
|
129
|
+
* Ordered by precedence, and the ordering is the reasoning. Any input that
|
|
130
|
+
* arrived from outside settles the question — this boundary is downstream of
|
|
131
|
+
* the cause, whatever else it may also be holding — so `handed` and `provided`
|
|
132
|
+
* outrank the state rungs. `unread` outranks `undetermined` for the same reason
|
|
133
|
+
* pointed the other way: silence is not agreement.
|
|
134
|
+
*/
|
|
135
|
+
export type PartingRung =
|
|
136
|
+
/** A named prop differs. The parent decided this; look up. */
|
|
137
|
+
'handed'
|
|
138
|
+
/** A context value differs. A provider above decided this; look up. */
|
|
139
|
+
| 'provided'
|
|
140
|
+
/**
|
|
141
|
+
* An inherited style value differs, and this boundary declares none of it.
|
|
142
|
+
* An ancestor's cascade decided this; look up.
|
|
143
|
+
*/
|
|
144
|
+
| 'inherited'
|
|
145
|
+
/** A `useSyncExternalStore` snapshot differs — the store moved, outside React. */
|
|
146
|
+
| 'external'
|
|
147
|
+
/** An own hook cell differs. **This is the cause.** */
|
|
148
|
+
| 'stateful'
|
|
149
|
+
/** The output moved and something this boundary depends on could not be read. */
|
|
150
|
+
| 'unread'
|
|
151
|
+
/** Every input was read, every input agreed, and the output moved anyway. */
|
|
152
|
+
| 'undetermined'
|
|
153
|
+
/** The boundary exists on one side only. */
|
|
154
|
+
| 'unpaired';
|
|
155
|
+
export interface MovedInput {
|
|
156
|
+
readonly kind: 'prop' | 'context' | 'hook' | 'inherited';
|
|
157
|
+
/**
|
|
158
|
+
* Prop name, context display name, hook name (`useState`), or — for
|
|
159
|
+
* `inherited` — the CSS property an ancestor decided.
|
|
160
|
+
*/
|
|
161
|
+
readonly name: string;
|
|
162
|
+
/** Hook call position — the index a reader gets counting down the component. */
|
|
163
|
+
readonly index?: number;
|
|
164
|
+
/**
|
|
165
|
+
* Explicitly `| undefined`, on {@link Delta}'s rule: an input present on one
|
|
166
|
+
* side only is the reading, not a field somebody forgot.
|
|
167
|
+
*/
|
|
168
|
+
readonly from?: Digest | undefined;
|
|
169
|
+
readonly to?: Digest | undefined;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Two snapshots of the same page, read for where they parted.
|
|
173
|
+
*
|
|
174
|
+
* Takes snapshots rather than trees, and takes them without refusing a subject
|
|
175
|
+
* mismatch, for `compareTrees`' stated reason: this produces an *explanation*,
|
|
176
|
+
* not a verdict, and two arms of an experiment are two subjects on purpose.
|
|
177
|
+
*/
|
|
178
|
+
export declare function partingOf(baseline: SemanticSnapshot, candidate: SemanticSnapshot): Parting;
|
|
179
|
+
//# sourceMappingURL=parting.d.ts.map
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { boundaries as componentBoundaries, UNATTRIBUTED } from '../attribute/boundary.js';
|
|
2
|
+
import { BANDS, bandOf } from './band.js';
|
|
3
|
+
import { compareTrees, matchTrees } from './diff/index.js';
|
|
4
|
+
import { cascadeInputs, declaredIn } from './cascade.js';
|
|
5
|
+
import { compareHoldings } from './holding-diff.js';
|
|
6
|
+
import { sameTree, sliceOf } from './slice.js';
|
|
7
|
+
/**
|
|
8
|
+
* Two snapshots of the same page, read for where they parted.
|
|
9
|
+
*
|
|
10
|
+
* Takes snapshots rather than trees, and takes them without refusing a subject
|
|
11
|
+
* mismatch, for `compareTrees`' stated reason: this produces an *explanation*,
|
|
12
|
+
* not a verdict, and two arms of an experiment are two subjects on purpose.
|
|
13
|
+
*/
|
|
14
|
+
export function partingOf(baseline, candidate) {
|
|
15
|
+
const comparison = compareTrees(baseline, candidate);
|
|
16
|
+
const matching = matchTrees(baseline.root, candidate.root);
|
|
17
|
+
const partner = new Map();
|
|
18
|
+
for (const [left, right] of matching.pairs)
|
|
19
|
+
partner.set(right, left);
|
|
20
|
+
const tree = sameTree(baseline.root, candidate.root);
|
|
21
|
+
const moved = comparison.deltas.length > 0;
|
|
22
|
+
const found = boundariesOf(baseline.root, candidate.root, partner, {
|
|
23
|
+
there: declaredIn(baseline),
|
|
24
|
+
here: declaredIn(candidate),
|
|
25
|
+
});
|
|
26
|
+
if (found === undefined) {
|
|
27
|
+
return {
|
|
28
|
+
slice: sliceOf(tree, undefined, moved),
|
|
29
|
+
identical: comparison.identical,
|
|
30
|
+
deltas: comparison.deltas,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
attribute(found, comparison.deltas);
|
|
34
|
+
const boundaries = found
|
|
35
|
+
.filter((entry) => entry.inputs.length > 0 || entry.owned.length > 0)
|
|
36
|
+
.map(settle);
|
|
37
|
+
const origins = boundaries.filter((entry) => entry.rung === 'stateful' || entry.rung === 'external' || entry.rung === 'undetermined');
|
|
38
|
+
return {
|
|
39
|
+
slice: sliceOf(tree, boundaries, moved),
|
|
40
|
+
identical: comparison.identical,
|
|
41
|
+
deltas: comparison.deltas,
|
|
42
|
+
boundaries,
|
|
43
|
+
...(origins.length > 0 ? { origins: [...origins].sort((a, b) => a.depth - b.depth) } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Every boundary in the candidate tree, in document order, paired with its
|
|
48
|
+
* baseline counterpart.
|
|
49
|
+
*
|
|
50
|
+
* Undefined when neither tree carried a holding at all, which is the ADR-0002
|
|
51
|
+
* distinction this whole module rests on: nothing to read is not nothing to
|
|
52
|
+
* report.
|
|
53
|
+
*/
|
|
54
|
+
function boundariesOf(baselineRoot, candidateRoot, partner, declared) {
|
|
55
|
+
const mine = boundaryNodes(candidateRoot);
|
|
56
|
+
const theirs = boundaryNodes(baselineRoot);
|
|
57
|
+
if (mine.length === 0 && theirs.length === 0)
|
|
58
|
+
return undefined;
|
|
59
|
+
const paired = pairBoundaries(mine, theirs, partner);
|
|
60
|
+
return mine.map((node) => {
|
|
61
|
+
const other = paired.get(node);
|
|
62
|
+
const moved = compareHoldings(other?.holding, node.holding);
|
|
63
|
+
const inputs = [
|
|
64
|
+
...moved.inputs,
|
|
65
|
+
...cascadeInputs(other, node, declared.there, declared.here),
|
|
66
|
+
];
|
|
67
|
+
return {
|
|
68
|
+
component: node.provenance?.owners[0]?.name ?? '(anonymous)',
|
|
69
|
+
path: node.path,
|
|
70
|
+
...(other === undefined ? {} : { basePath: other.path }),
|
|
71
|
+
depth: depthOf(node.path),
|
|
72
|
+
inputs,
|
|
73
|
+
unread: moved.unread,
|
|
74
|
+
paired: other !== undefined,
|
|
75
|
+
owned: [],
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Every node where a component starts, in document order.
|
|
81
|
+
*
|
|
82
|
+
* Read from provenance and not from `holding`, which is the difference between
|
|
83
|
+
* this module working on one collector and working on all of them. A holding is
|
|
84
|
+
* what a *framework adapter* managed to read at a boundary — props values, hook
|
|
85
|
+
* cells — and only `@variance-authority/unit-test` supplies one today. Keying the
|
|
86
|
+
* boundary set on it made every rung unreachable from a browser run, including
|
|
87
|
+
* the two that need no adapter at all: the ancestor cascade, which is read from
|
|
88
|
+
* `styleProvenance`, and the tree comparison, which is read from the owner
|
|
89
|
+
* chains. Those runs got `unread` — "nothing can be said about why" — while
|
|
90
|
+
* holding the evidence to say it.
|
|
91
|
+
*
|
|
92
|
+
* {@link componentBoundaries} is the shared definition, so a boundary here is
|
|
93
|
+
* the same node the component hashes and the wiring band call one. It reports a
|
|
94
|
+
* node once per component that opens there; the nodes are what this needs, and
|
|
95
|
+
* `boundariesOf` names each by its innermost owner exactly as before.
|
|
96
|
+
*
|
|
97
|
+
* A node with a holding and no provenance is still kept. That is an adapter that
|
|
98
|
+
* read the fiber's values but not its owner chain, and dropping its evidence
|
|
99
|
+
* because of a second failure would lose the one rung it can still reach.
|
|
100
|
+
*/
|
|
101
|
+
function boundaryNodes(root) {
|
|
102
|
+
const found = new Set();
|
|
103
|
+
for (const boundary of componentBoundaries(root)) {
|
|
104
|
+
// `(unattributed)` is the bucket for nodes whose owner chain broke, not a
|
|
105
|
+
// component, and `causesBetween` refuses it for the same reason: a boundary
|
|
106
|
+
// reported there would name a component nobody wrote and pool unrelated
|
|
107
|
+
// parts of the page under one name.
|
|
108
|
+
if (boundary.component === UNATTRIBUTED)
|
|
109
|
+
continue;
|
|
110
|
+
found.add(boundary.node);
|
|
111
|
+
}
|
|
112
|
+
const holders = (node) => {
|
|
113
|
+
if (node.holding !== undefined)
|
|
114
|
+
found.add(node);
|
|
115
|
+
for (const child of node.children)
|
|
116
|
+
holders(child);
|
|
117
|
+
};
|
|
118
|
+
holders(root);
|
|
119
|
+
return [...found].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Pair boundaries, preferring the tree matcher and falling back to the owner
|
|
123
|
+
* chain.
|
|
124
|
+
*
|
|
125
|
+
* The fallback is not a nicety, it is the case the whole module exists for. A
|
|
126
|
+
* component that took the other branch returns a `<span>` where it returned a
|
|
127
|
+
* `<p>`, and no host-level matcher pairs those: the tag is the thing that
|
|
128
|
+
* changed, and every identity key ends in it. Reporting the boundary as present
|
|
129
|
+
* on one side only would throw away the props and the hook cells at exactly the
|
|
130
|
+
* moment they are the answer — `Summary` did not appear, it *chose differently*,
|
|
131
|
+
* and the evidence for that is sitting in a holding nobody joined.
|
|
132
|
+
*
|
|
133
|
+
* The chain — the boundary's owner names, outermost component included — is
|
|
134
|
+
* stable under precisely that change, since neither the branch nor the tag
|
|
135
|
+
* appears in it. Ties inside one chain are broken by document order, which is
|
|
136
|
+
* positional and therefore shifts when a sibling is inserted; that is the same
|
|
137
|
+
* cost `match.ts` pays for the same reason, and it is bounded here to
|
|
138
|
+
* components sharing one chain that the matcher already failed to pair.
|
|
139
|
+
*/
|
|
140
|
+
function pairBoundaries(mine, theirs, partner) {
|
|
141
|
+
const paired = new Map();
|
|
142
|
+
const taken = new Set();
|
|
143
|
+
for (const node of mine) {
|
|
144
|
+
const other = partner.get(node);
|
|
145
|
+
if (other?.holding === undefined || taken.has(other))
|
|
146
|
+
continue;
|
|
147
|
+
paired.set(node, other);
|
|
148
|
+
taken.add(other);
|
|
149
|
+
}
|
|
150
|
+
const spare = new Map();
|
|
151
|
+
for (const node of theirs) {
|
|
152
|
+
if (taken.has(node))
|
|
153
|
+
continue;
|
|
154
|
+
const key = chainOf(node);
|
|
155
|
+
const bucket = spare.get(key);
|
|
156
|
+
if (bucket === undefined)
|
|
157
|
+
spare.set(key, [node]);
|
|
158
|
+
else
|
|
159
|
+
bucket.push(node);
|
|
160
|
+
}
|
|
161
|
+
for (const node of mine) {
|
|
162
|
+
if (paired.has(node))
|
|
163
|
+
continue;
|
|
164
|
+
const bucket = spare.get(chainOf(node));
|
|
165
|
+
const other = bucket?.shift();
|
|
166
|
+
if (other !== undefined)
|
|
167
|
+
paired.set(node, other);
|
|
168
|
+
}
|
|
169
|
+
return paired;
|
|
170
|
+
}
|
|
171
|
+
function chainOf(node) {
|
|
172
|
+
return (node.provenance?.owners ?? []).map((owner) => owner.name).join('>');
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Give each delta to the deepest boundary that encloses it.
|
|
176
|
+
*
|
|
177
|
+
* By prefix rather than by scanning every boundary: a node's path *is* its
|
|
178
|
+
* ancestry, so walking it up one segment at a time reaches the owning boundary
|
|
179
|
+
* in as many steps as the node is deep. A `node-removed` delta carries a
|
|
180
|
+
* baseline path, so it is resolved in baseline space — mixing the two spaces
|
|
181
|
+
* would silently hand a removal to whichever candidate node now sits at that
|
|
182
|
+
* address, which is the exact misattribution positional diffing is famous for.
|
|
183
|
+
*/
|
|
184
|
+
function attribute(drafts, deltas) {
|
|
185
|
+
const here = new Map();
|
|
186
|
+
const there = new Map();
|
|
187
|
+
for (const draft of drafts) {
|
|
188
|
+
here.set(draft.path, draft);
|
|
189
|
+
if (draft.basePath !== undefined)
|
|
190
|
+
there.set(draft.basePath, draft);
|
|
191
|
+
}
|
|
192
|
+
for (const delta of deltas) {
|
|
193
|
+
const index = delta.kind === 'node-removed' ? there : here;
|
|
194
|
+
for (let path = delta.path; path !== undefined; path = parentOf(path)) {
|
|
195
|
+
const owner = index.get(path);
|
|
196
|
+
if (owner !== undefined) {
|
|
197
|
+
owner.owned.push(delta);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Apply the precedence in {@link PartingRung} and freeze the row. */
|
|
204
|
+
function settle(draft) {
|
|
205
|
+
const bands = BANDS.filter((band) => draft.owned.some((delta) => bandOf(delta.kind) === band));
|
|
206
|
+
const moved = [
|
|
207
|
+
...new Set(draft.owned.map((delta) => delta.property).filter((name) => name !== undefined)),
|
|
208
|
+
].sort();
|
|
209
|
+
return {
|
|
210
|
+
component: draft.component,
|
|
211
|
+
path: draft.path,
|
|
212
|
+
depth: draft.depth,
|
|
213
|
+
rung: rungOf(draft),
|
|
214
|
+
inputs: draft.inputs,
|
|
215
|
+
deltas: draft.owned.length,
|
|
216
|
+
bands,
|
|
217
|
+
...(moved.length > 0 ? { moved } : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function rungOf(draft) {
|
|
221
|
+
if (!draft.paired)
|
|
222
|
+
return 'unpaired';
|
|
223
|
+
if (draft.inputs.some((input) => input.kind === 'prop'))
|
|
224
|
+
return 'handed';
|
|
225
|
+
if (draft.inputs.some((input) => input.kind === 'context'))
|
|
226
|
+
return 'provided';
|
|
227
|
+
if (draft.inputs.some((input) => input.kind === 'inherited'))
|
|
228
|
+
return 'inherited';
|
|
229
|
+
if (draft.inputs.some((input) => input.name === 'useSyncExternalStore'))
|
|
230
|
+
return 'external';
|
|
231
|
+
if (draft.inputs.length > 0)
|
|
232
|
+
return 'stateful';
|
|
233
|
+
return draft.unread ? 'unread' : 'undetermined';
|
|
234
|
+
}
|
|
235
|
+
function depthOf(path) {
|
|
236
|
+
return path === '' ? 0 : path.split('/').length;
|
|
237
|
+
}
|
|
238
|
+
function parentOf(path) {
|
|
239
|
+
if (path === '')
|
|
240
|
+
return undefined;
|
|
241
|
+
const cut = path.lastIndexOf('/');
|
|
242
|
+
return cut === -1 ? '' : path.slice(0, cut);
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=parting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parting.js","sourceRoot":"","sources":["../../src/compare/parting.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,IAAI,mBAAmB,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG3F,OAAO,EAAE,KAAK,EAAE,MAAM,EAAa,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAc,MAAM,iBAAiB,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAqB,MAAM,YAAY,CAAC;AAyLlE;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,QAA0B,EAAE,SAA2B;IAC/E,MAAM,UAAU,GAAG,YAAY,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IAE3D,MAAM,OAAO,GAAG,IAAI,GAAG,EAA8B,CAAC;IACtD,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK;QAAE,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAErE,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IACrD,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IAE3C,MAAM,KAAK,GAAG,YAAY,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE;QACjE,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC;QAC3B,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC;KAC5B,CAAC,CAAC;IACH,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO;YACL,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC;YACtC,SAAS,EAAE,UAAU,CAAC,SAAS;YAC/B,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC;IACJ,CAAC;IAED,SAAS,CAAC,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IAEpC,MAAM,UAAU,GAAG,KAAK;SACrB,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;SACpE,GAAG,CAAC,MAAM,CAAC,CAAC;IAEf,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAC/B,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,CACnG,CAAC;IAEF,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,CAAC;QACvC,SAAS,EAAE,UAAU,CAAC,SAAS;QAC/B,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,UAAU;QACV,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3F,CAAC;AACJ,CAAC;AAcD;;;;;;;GAOG;AACH,SAAS,YAAY,CACnB,YAA0B,EAC1B,aAA2B,EAC3B,OAAgD,EAChD,QAAqF;IAErF,MAAM,IAAI,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAE/D,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IAErD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACvB,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAG;YACb,GAAG,KAAK,CAAC,MAAM;YACf,GAAG,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC;SAC7D,CAAC;QACF,OAAO;YACL,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,aAAa;YAC5D,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;YACxD,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;YACzB,MAAM;YACN,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,MAAM,EAAE,KAAK,KAAK,SAAS;YAC3B,KAAK,EAAE,EAAE;SACV,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,SAAS,aAAa,CAAC,IAAkB;IACvC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgB,CAAC;IACtC,KAAK,MAAM,QAAQ,IAAI,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC;QACjD,0EAA0E;QAC1E,4EAA4E;QAC5E,wEAAwE;QACxE,oCAAoC;QACpC,IAAI,QAAQ,CAAC,SAAS,KAAK,YAAY;YAAE,SAAS;QAClD,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,IAAkB,EAAQ,EAAE;QAC3C,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACpD,CAAC,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC;IAEd,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrF,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAS,cAAc,CACrB,IAA6B,EAC7B,MAA+B,EAC/B,OAAgD;IAEhD,MAAM,MAAM,GAAG,IAAI,GAAG,EAA8B,CAAC;IACrD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgB,CAAC;IAEtC,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,KAAK,EAAE,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,SAAS;QAC/D,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACxB,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;IAChD,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAC9B,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,MAAM,KAAK,SAAS;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;;YAC5C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAC/B,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC;QAC9B,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,OAAO,CAAC,IAAkB;IACjC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC9E,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,SAAS,CAAC,MAAwB,EAAE,MAAwB;IACnE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAmB,CAAC;IACxC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAmB,CAAC;IACzC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC5B,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACrE,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;QAC3D,KAAK,IAAI,IAAI,GAAyB,KAAK,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,EAAE,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5F,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACxB,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,SAAS,MAAM,CAAC,KAAY;IAC1B,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC;IAE/F,MAAM,KAAK,GAAG;QACZ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;KAC5F,CAAC,IAAI,EAAE,CAAC;IAET,OAAO;QACL,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC;QACnB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM;QAC1B,KAAK;QACL,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACvC,CAAC;AACJ,CAAC;AAED,SAAS,MAAM,CAAC,KAAY;IAC1B,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,UAAU,CAAC;IACrC,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC;QAAE,OAAO,QAAQ,CAAC;IACzE,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC;QAAE,OAAO,UAAU,CAAC;IAC9E,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC;QAAE,OAAO,WAAW,CAAC;IACjF,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,sBAAsB,CAAC;QAAE,OAAO,UAAU,CAAC;IAC3F,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,UAAU,CAAC;IAC/C,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC;AAClD,CAAC;AAGD,SAAS,OAAO,CAAC,IAAc;IAC7B,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;AAClD,CAAC;AAED,SAAS,QAAQ,CAAC,IAAc;IAC9B,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IAClC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC9C,CAAC","sourcesContent":["import { boundaries as componentBoundaries, UNATTRIBUTED } from '../attribute/boundary.js';\nimport type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticNode, SemanticSnapshot } from '../format/snapshot.js';\nimport { BANDS, bandOf, type Band } from './band.js';\nimport { compareTrees, matchTrees, type Delta } from './diff/index.js';\nimport { cascadeInputs, declaredIn } from './cascade.js';\nimport { compareHoldings } from './holding-diff.js';\nimport { sameTree, sliceOf, type PartingSlice } from './slice.js';\n\n/**\n * Where two readings of one page parted.\n *\n * A diff says a `<div>` rendered a `<p>` on one side and a `<span>` on the\n * other. That is a symptom, and acting on it means reading the component,\n * guessing which branch ran, and guessing why. This says: `Cart`'s `useState` at\n * call 2 held different values, so `Summary` was handed a different `total`, so\n * the tag moved. Same evidence, three rungs up.\n *\n * The rung it can reach is decided by how much of the boundary was readable, and\n * the difference between the top two rungs is the whole point:\n *\n * - **collateral** — a prop or a context this boundary *received* moved, so the\n * difference arrived from outside and something above it is the cause.\n * - **origin** — every input this boundary received held, and its own retained\n * state moved. Nothing above it explains this; it is the answer.\n *\n * The mechanism is one rule applied at every boundary: *a component whose\n * inputs agreed and whose output moved decided differently.* Walking the tree\n * for the shallowest boundary where that holds is what turns a page of deltas\n * into one sentence — and the same rule, run over two readings of one page\n * rather than two arms of an experiment, is what separates a flake from a\n * change. A flake is the case where nothing readable moved at all\n * ({@link PartingRung} `undetermined`), and it is only nameable because\n * `unread` is a rung of its own: an accusation of nondeterminism may only be\n * made about inputs somebody actually read (ADR-0002).\n *\n * Not `attribute/composition.ts`'s `Divergence`, which is the same suspicion\n * arrived at from the other side: one component rendering two ways from one\n * props digest *within* a run, found by counting sites. That one can say a\n * component's inputs do not determine its output; this one can say which input.\n *\n * Nothing here reaches a hash. {@link Holding} is evidence carried beside the\n * snapshot, exactly as `styleProvenance` is, and for the reason `wiring.ts`\n * gives: a hook's value is the thing that legitimately differs between two\n * readings of one page, so a band carrying it would be a flake generator wearing\n * a band's name. This module is the reader that value was kept for.\n */\nexport interface Parting {\n /**\n * What kind of parting this is, decided before which input moved.\n *\n * The triage line. `refactor` and `settled` are pages nobody needs to open,\n * `flake` is a page whose baseline is the problem, and `variation` is the\n * only one where the rungs below are worth reading.\n */\n readonly slice: PartingSlice;\n\n /** `true` when both render hashes agree. Inputs may still have moved. */\n readonly identical: boolean;\n\n /** Every delta, unchanged, so a caller need not run the comparison twice. */\n readonly deltas: readonly Delta[];\n\n /**\n * Boundaries where something moved — an input, an output, or both — in\n * document order.\n *\n * Absent, never `[]`, when no node on either side started a component: a page\n * read with no framework adapter has not been found to agree, it has not been\n * asked (ADR-0002). Present with every boundary at rung `unread` is the next\n * reading up — the components were found and their inputs were not.\n */\n readonly boundaries?: readonly PartedBoundary[];\n\n /**\n * The boundaries the difference started at, shallowest first.\n *\n * A subset of {@link boundaries}: those whose output moved without an\n * incoming input to explain it. Usually one. More than one means the two\n * renders differ in more than one place, which is a real answer rather than a\n * failure to narrow.\n */\n readonly origins?: readonly PartedBoundary[];\n}\n\n/**\n * One component boundary, and what moved at it.\n *\n * A boundary is a component's root host node — `componentFiberOf`'s definition,\n * shared with the wiring band so the two can never disagree about where a\n * component starts. A component that vanished entirely has no boundary here: its\n * nodes leave `node-removed` deltas that attribute to the *parent* boundary,\n * which is the correct answer, because deciding not to render a child is\n * something the parent did.\n */\nexport interface PartedBoundary {\n /** `displayName`, or `(anonymous)` when provenance did not name it. */\n readonly component: string;\n\n /** Path in the candidate tree. */\n readonly path: NodePath;\n\n /** Depth of {@link path}, so callers can order without re-parsing it. */\n readonly depth: number;\n\n readonly rung: PartingRung;\n\n /**\n * Inputs that differ: props, then contexts, then hook calls in order, then\n * the inherited properties no declaration at this boundary accounts for.\n */\n readonly inputs: readonly MovedInput[];\n\n /**\n * Deltas at or under this boundary that no nested boundary owns.\n *\n * Zero is meaningful and is not a filtered-out row: a boundary with moved\n * inputs and no deltas of its own is the negative result an experiment\n * usually wants — this arm was assigned differently and rendered the same.\n */\n readonly deltas: number;\n\n /** Bands those deltas fall in, in band order. */\n readonly bands: readonly Band[];\n\n /**\n * The properties the owned deltas named, deduplicated and in code-unit order.\n *\n * The last joint of the chain the rungs above climb: a hook cell moved, a prop\n * carried it down, and *this* is what the prop turned into on the page —\n * `color`, `padding-top`, `width`. A reader chasing a visual regression is\n * looking for this list, and `deltas: 7` alone sends them back to the delta\n * array to assemble it.\n *\n * Absent when no owned delta named a property, which is a reading rather than\n * a gap: a removed node and a changed accessible name are whole-node facts and\n * have no property to name. `deltas` and `bands` still say what happened\n * there.\n */\n readonly moved?: readonly string[];\n}\n\n/**\n * How far up the difference could be traced at one boundary.\n *\n * Ordered by precedence, and the ordering is the reasoning. Any input that\n * arrived from outside settles the question — this boundary is downstream of\n * the cause, whatever else it may also be holding — so `handed` and `provided`\n * outrank the state rungs. `unread` outranks `undetermined` for the same reason\n * pointed the other way: silence is not agreement.\n */\nexport type PartingRung =\n /** A named prop differs. The parent decided this; look up. */\n | 'handed'\n /** A context value differs. A provider above decided this; look up. */\n | 'provided'\n /**\n * An inherited style value differs, and this boundary declares none of it.\n * An ancestor's cascade decided this; look up.\n */\n | 'inherited'\n /** A `useSyncExternalStore` snapshot differs — the store moved, outside React. */\n | 'external'\n /** An own hook cell differs. **This is the cause.** */\n | 'stateful'\n /** The output moved and something this boundary depends on could not be read. */\n | 'unread'\n /** Every input was read, every input agreed, and the output moved anyway. */\n | 'undetermined'\n /** The boundary exists on one side only. */\n | 'unpaired';\n\nexport interface MovedInput {\n readonly kind: 'prop' | 'context' | 'hook' | 'inherited';\n\n /**\n * Prop name, context display name, hook name (`useState`), or — for\n * `inherited` — the CSS property an ancestor decided.\n */\n readonly name: string;\n\n /** Hook call position — the index a reader gets counting down the component. */\n readonly index?: number;\n\n /**\n * Explicitly `| undefined`, on {@link Delta}'s rule: an input present on one\n * side only is the reading, not a field somebody forgot.\n */\n readonly from?: Digest | undefined;\n readonly to?: Digest | undefined;\n}\n\n/**\n * Two snapshots of the same page, read for where they parted.\n *\n * Takes snapshots rather than trees, and takes them without refusing a subject\n * mismatch, for `compareTrees`' stated reason: this produces an *explanation*,\n * not a verdict, and two arms of an experiment are two subjects on purpose.\n */\nexport function partingOf(baseline: SemanticSnapshot, candidate: SemanticSnapshot): Parting {\n const comparison = compareTrees(baseline, candidate);\n const matching = matchTrees(baseline.root, candidate.root);\n\n const partner = new Map<SemanticNode, SemanticNode>();\n for (const [left, right] of matching.pairs) partner.set(right, left);\n\n const tree = sameTree(baseline.root, candidate.root);\n const moved = comparison.deltas.length > 0;\n\n const found = boundariesOf(baseline.root, candidate.root, partner, {\n there: declaredIn(baseline),\n here: declaredIn(candidate),\n });\n if (found === undefined) {\n return {\n slice: sliceOf(tree, undefined, moved),\n identical: comparison.identical,\n deltas: comparison.deltas,\n };\n }\n\n attribute(found, comparison.deltas);\n\n const boundaries = found\n .filter((entry) => entry.inputs.length > 0 || entry.owned.length > 0)\n .map(settle);\n\n const origins = boundaries.filter(\n (entry) => entry.rung === 'stateful' || entry.rung === 'external' || entry.rung === 'undetermined',\n );\n\n return {\n slice: sliceOf(tree, boundaries, moved),\n identical: comparison.identical,\n deltas: comparison.deltas,\n boundaries,\n ...(origins.length > 0 ? { origins: [...origins].sort((a, b) => a.depth - b.depth) } : {}),\n };\n}\n\n/** A boundary mid-construction, before its deltas are counted. */\ninterface Draft {\n readonly component: string;\n readonly path: NodePath;\n readonly basePath?: NodePath;\n readonly depth: number;\n readonly inputs: readonly MovedInput[];\n readonly unread: boolean;\n readonly paired: boolean;\n readonly owned: Delta[];\n}\n\n/**\n * Every boundary in the candidate tree, in document order, paired with its\n * baseline counterpart.\n *\n * Undefined when neither tree carried a holding at all, which is the ADR-0002\n * distinction this whole module rests on: nothing to read is not nothing to\n * report.\n */\nfunction boundariesOf(\n baselineRoot: SemanticNode,\n candidateRoot: SemanticNode,\n partner: ReadonlyMap<SemanticNode, SemanticNode>,\n declared: { readonly there: ReadonlySet<string>; readonly here: ReadonlySet<string> },\n): Draft[] | undefined {\n const mine = boundaryNodes(candidateRoot);\n const theirs = boundaryNodes(baselineRoot);\n if (mine.length === 0 && theirs.length === 0) return undefined;\n\n const paired = pairBoundaries(mine, theirs, partner);\n\n return mine.map((node) => {\n const other = paired.get(node);\n const moved = compareHoldings(other?.holding, node.holding);\n const inputs = [\n ...moved.inputs,\n ...cascadeInputs(other, node, declared.there, declared.here),\n ];\n return {\n component: node.provenance?.owners[0]?.name ?? '(anonymous)',\n path: node.path,\n ...(other === undefined ? {} : { basePath: other.path }),\n depth: depthOf(node.path),\n inputs,\n unread: moved.unread,\n paired: other !== undefined,\n owned: [],\n };\n });\n}\n\n/**\n * Every node where a component starts, in document order.\n *\n * Read from provenance and not from `holding`, which is the difference between\n * this module working on one collector and working on all of them. A holding is\n * what a *framework adapter* managed to read at a boundary — props values, hook\n * cells — and only `@variance-authority/unit-test` supplies one today. Keying the\n * boundary set on it made every rung unreachable from a browser run, including\n * the two that need no adapter at all: the ancestor cascade, which is read from\n * `styleProvenance`, and the tree comparison, which is read from the owner\n * chains. Those runs got `unread` — \"nothing can be said about why\" — while\n * holding the evidence to say it.\n *\n * {@link componentBoundaries} is the shared definition, so a boundary here is\n * the same node the component hashes and the wiring band call one. It reports a\n * node once per component that opens there; the nodes are what this needs, and\n * `boundariesOf` names each by its innermost owner exactly as before.\n *\n * A node with a holding and no provenance is still kept. That is an adapter that\n * read the fiber's values but not its owner chain, and dropping its evidence\n * because of a second failure would lose the one rung it can still reach.\n */\nfunction boundaryNodes(root: SemanticNode): readonly SemanticNode[] {\n const found = new Set<SemanticNode>();\n for (const boundary of componentBoundaries(root)) {\n // `(unattributed)` is the bucket for nodes whose owner chain broke, not a\n // component, and `causesBetween` refuses it for the same reason: a boundary\n // reported there would name a component nobody wrote and pool unrelated\n // parts of the page under one name.\n if (boundary.component === UNATTRIBUTED) continue;\n found.add(boundary.node);\n }\n\n const holders = (node: SemanticNode): void => {\n if (node.holding !== undefined) found.add(node);\n for (const child of node.children) holders(child);\n };\n holders(root);\n\n return [...found].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n}\n\n/**\n * Pair boundaries, preferring the tree matcher and falling back to the owner\n * chain.\n *\n * The fallback is not a nicety, it is the case the whole module exists for. A\n * component that took the other branch returns a `<span>` where it returned a\n * `<p>`, and no host-level matcher pairs those: the tag is the thing that\n * changed, and every identity key ends in it. Reporting the boundary as present\n * on one side only would throw away the props and the hook cells at exactly the\n * moment they are the answer — `Summary` did not appear, it *chose differently*,\n * and the evidence for that is sitting in a holding nobody joined.\n *\n * The chain — the boundary's owner names, outermost component included — is\n * stable under precisely that change, since neither the branch nor the tag\n * appears in it. Ties inside one chain are broken by document order, which is\n * positional and therefore shifts when a sibling is inserted; that is the same\n * cost `match.ts` pays for the same reason, and it is bounded here to\n * components sharing one chain that the matcher already failed to pair.\n */\nfunction pairBoundaries(\n mine: readonly SemanticNode[],\n theirs: readonly SemanticNode[],\n partner: ReadonlyMap<SemanticNode, SemanticNode>,\n): ReadonlyMap<SemanticNode, SemanticNode> {\n const paired = new Map<SemanticNode, SemanticNode>();\n const taken = new Set<SemanticNode>();\n\n for (const node of mine) {\n const other = partner.get(node);\n if (other?.holding === undefined || taken.has(other)) continue;\n paired.set(node, other);\n taken.add(other);\n }\n\n const spare = new Map<string, SemanticNode[]>();\n for (const node of theirs) {\n if (taken.has(node)) continue;\n const key = chainOf(node);\n const bucket = spare.get(key);\n if (bucket === undefined) spare.set(key, [node]);\n else bucket.push(node);\n }\n\n for (const node of mine) {\n if (paired.has(node)) continue;\n const bucket = spare.get(chainOf(node));\n const other = bucket?.shift();\n if (other !== undefined) paired.set(node, other);\n }\n\n return paired;\n}\n\nfunction chainOf(node: SemanticNode): string {\n return (node.provenance?.owners ?? []).map((owner) => owner.name).join('>');\n}\n\n/**\n * Give each delta to the deepest boundary that encloses it.\n *\n * By prefix rather than by scanning every boundary: a node's path *is* its\n * ancestry, so walking it up one segment at a time reaches the owning boundary\n * in as many steps as the node is deep. A `node-removed` delta carries a\n * baseline path, so it is resolved in baseline space — mixing the two spaces\n * would silently hand a removal to whichever candidate node now sits at that\n * address, which is the exact misattribution positional diffing is famous for.\n */\nfunction attribute(drafts: readonly Draft[], deltas: readonly Delta[]): void {\n const here = new Map<NodePath, Draft>();\n const there = new Map<NodePath, Draft>();\n for (const draft of drafts) {\n here.set(draft.path, draft);\n if (draft.basePath !== undefined) there.set(draft.basePath, draft);\n }\n\n for (const delta of deltas) {\n const index = delta.kind === 'node-removed' ? there : here;\n for (let path: NodePath | undefined = delta.path; path !== undefined; path = parentOf(path)) {\n const owner = index.get(path);\n if (owner !== undefined) {\n owner.owned.push(delta);\n break;\n }\n }\n }\n}\n\n/** Apply the precedence in {@link PartingRung} and freeze the row. */\nfunction settle(draft: Draft): PartedBoundary {\n const bands = BANDS.filter((band) => draft.owned.some((delta) => bandOf(delta.kind) === band));\n\n const moved = [\n ...new Set(draft.owned.map((delta) => delta.property).filter((name) => name !== undefined)),\n ].sort();\n\n return {\n component: draft.component,\n path: draft.path,\n depth: draft.depth,\n rung: rungOf(draft),\n inputs: draft.inputs,\n deltas: draft.owned.length,\n bands,\n ...(moved.length > 0 ? { moved } : {}),\n };\n}\n\nfunction rungOf(draft: Draft): PartingRung {\n if (!draft.paired) return 'unpaired';\n if (draft.inputs.some((input) => input.kind === 'prop')) return 'handed';\n if (draft.inputs.some((input) => input.kind === 'context')) return 'provided';\n if (draft.inputs.some((input) => input.kind === 'inherited')) return 'inherited';\n if (draft.inputs.some((input) => input.name === 'useSyncExternalStore')) return 'external';\n if (draft.inputs.length > 0) return 'stateful';\n return draft.unread ? 'unread' : 'undetermined';\n}\n\n\nfunction depthOf(path: NodePath): number {\n return path === '' ? 0 : path.split('/').length;\n}\n\nfunction parentOf(path: NodePath): NodePath | undefined {\n if (path === '') return undefined;\n const cut = path.lastIndexOf('/');\n return cut === -1 ? '' : path.slice(0, cut);\n}\n"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { SemanticNode } from '../format/snapshot.js';
|
|
2
|
+
import type { PartedBoundary } from './parting.js';
|
|
3
|
+
/**
|
|
4
|
+
* What kind of parting this is, before anything about which one.
|
|
5
|
+
*
|
|
6
|
+
* The rungs in `parting.ts` answer *which input moved*. This answers the
|
|
7
|
+
* question asked first and answered least: **is this worth looking at at all.**
|
|
8
|
+
* Three facts are read independently — did the component tree move, did any
|
|
9
|
+
* input move, did the output move — and the eight combinations collapse to
|
|
10
|
+
* seven sentences, five of which are a triage decision on their own:
|
|
11
|
+
*
|
|
12
|
+
* | component tree | inputs | output | slice |
|
|
13
|
+
* |---|---|---|---|
|
|
14
|
+
* | same | same | **moved** | `flake` |
|
|
15
|
+
* | any | **moved** | **moved** | `variation` |
|
|
16
|
+
* | **moved** | same | **moved** | `reshaped` |
|
|
17
|
+
* | **moved** | any | same | `refactor` |
|
|
18
|
+
* | same | **moved** | same | `absorbed` |
|
|
19
|
+
* | same | same | same | `settled` |
|
|
20
|
+
* | unread | — | **moved** | `unread` |
|
|
21
|
+
*
|
|
22
|
+
* `reshaped` is the row that used to be missing. It was answered `variation`,
|
|
23
|
+
* whose sentence says an input moved — and the whole point of this table is
|
|
24
|
+
* that in that row no input did. Two readings whose component trees are not the
|
|
25
|
+
* same tree, reached from inputs that all agreed, is a component that chose a
|
|
26
|
+
* different shape: a branch taken differently between two arms, or a rewrite
|
|
27
|
+
* between two revisions. Either way the tree is the finding and the rungs below
|
|
28
|
+
* have nothing to add, because there is no moved input for them to name.
|
|
29
|
+
*
|
|
30
|
+
* `refactor` is the one that pays for the other six. A component tree that
|
|
31
|
+
* moved while the page did not is the receipt a refactor never gets: the
|
|
32
|
+
* screenshots match, which is what a pixel differ says and all it says, and
|
|
33
|
+
* *the components underneath were rewritten*, which is the fact somebody wanted
|
|
34
|
+
* confirmed before merging.
|
|
35
|
+
*/
|
|
36
|
+
export type PartingSlice =
|
|
37
|
+
/** Nothing moved: not the tree, not an input, not the output. */
|
|
38
|
+
'settled'
|
|
39
|
+
/** An input moved and the output followed. The ordinary parting. */
|
|
40
|
+
| 'variation'
|
|
41
|
+
/** Every input agreed, the tree held, and the output moved anyway. */
|
|
42
|
+
| 'flake'
|
|
43
|
+
/** The component tree is a different tree, no input moved, and the output followed. */
|
|
44
|
+
| 'reshaped'
|
|
45
|
+
/** The component tree moved and the output did not. */
|
|
46
|
+
| 'refactor'
|
|
47
|
+
/** An input moved and the output did not: the component ignored it. */
|
|
48
|
+
| 'absorbed'
|
|
49
|
+
/** The output moved and what would explain it was not read. */
|
|
50
|
+
| 'unread';
|
|
51
|
+
/**
|
|
52
|
+
* Decide the slice from three independent readings.
|
|
53
|
+
*
|
|
54
|
+
* `tree` is `undefined` when neither side carried provenance — not `false`.
|
|
55
|
+
* A run that never asked what components were there has not found them equal,
|
|
56
|
+
* and both `refactor` and `flake` are claims about the component tree that such
|
|
57
|
+
* a run is not entitled to make (ADR-0002).
|
|
58
|
+
*/
|
|
59
|
+
export declare function sliceOf(tree: boolean | undefined, boundaries: readonly PartedBoundary[] | undefined, moved: boolean): PartingSlice;
|
|
60
|
+
/**
|
|
61
|
+
* Whether the two component trees are the same tree.
|
|
62
|
+
*
|
|
63
|
+
* The signature is the owner chain plus the wiring at each node that carries
|
|
64
|
+
* one, in document order — the shape `wiringOf` already records, which is what
|
|
65
|
+
* makes this a reading of the fiber rather than of the markup it produced. A
|
|
66
|
+
* renamed component, an added `memo`, a hook inserted, a `key` changed and a
|
|
67
|
+
* subtree moved all land here; a repainted pixel does not.
|
|
68
|
+
*
|
|
69
|
+
* `undefined` when neither side carried provenance at any node. A page read
|
|
70
|
+
* without a framework adapter has no component tree to compare, which is a
|
|
71
|
+
* different answer from two trees that matched.
|
|
72
|
+
*/
|
|
73
|
+
export declare function sameTree(baseline: SemanticNode, candidate: SemanticNode): boolean | undefined;
|
|
74
|
+
//# sourceMappingURL=slice.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide the slice from three independent readings.
|
|
3
|
+
*
|
|
4
|
+
* `tree` is `undefined` when neither side carried provenance — not `false`.
|
|
5
|
+
* A run that never asked what components were there has not found them equal,
|
|
6
|
+
* and both `refactor` and `flake` are claims about the component tree that such
|
|
7
|
+
* a run is not entitled to make (ADR-0002).
|
|
8
|
+
*/
|
|
9
|
+
export function sliceOf(tree, boundaries, moved) {
|
|
10
|
+
// Nothing was read, so nothing may be alleged. `flake` in particular is an
|
|
11
|
+
// accusation, and it is the one this project must never make on silence.
|
|
12
|
+
if (boundaries === undefined)
|
|
13
|
+
return moved ? 'unread' : 'settled';
|
|
14
|
+
const inputs = boundaries.some((boundary) => boundary.inputs.length > 0);
|
|
15
|
+
if (!moved) {
|
|
16
|
+
if (tree === false)
|
|
17
|
+
return 'refactor';
|
|
18
|
+
return inputs ? 'absorbed' : 'settled';
|
|
19
|
+
}
|
|
20
|
+
if (inputs)
|
|
21
|
+
return 'variation';
|
|
22
|
+
if (tree === false)
|
|
23
|
+
return 'reshaped';
|
|
24
|
+
// `flake` is the accusation, and it rests on the component tree having held.
|
|
25
|
+
// A run that read holdings but no provenance never established that, so it
|
|
26
|
+
// gets the rung that says so rather than the one that blames the page.
|
|
27
|
+
if (tree === undefined)
|
|
28
|
+
return 'unread';
|
|
29
|
+
return 'flake';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether the two component trees are the same tree.
|
|
33
|
+
*
|
|
34
|
+
* The signature is the owner chain plus the wiring at each node that carries
|
|
35
|
+
* one, in document order — the shape `wiringOf` already records, which is what
|
|
36
|
+
* makes this a reading of the fiber rather than of the markup it produced. A
|
|
37
|
+
* renamed component, an added `memo`, a hook inserted, a `key` changed and a
|
|
38
|
+
* subtree moved all land here; a repainted pixel does not.
|
|
39
|
+
*
|
|
40
|
+
* `undefined` when neither side carried provenance at any node. A page read
|
|
41
|
+
* without a framework adapter has no component tree to compare, which is a
|
|
42
|
+
* different answer from two trees that matched.
|
|
43
|
+
*/
|
|
44
|
+
export function sameTree(baseline, candidate) {
|
|
45
|
+
const left = signature(baseline, []);
|
|
46
|
+
const right = signature(candidate, []);
|
|
47
|
+
if (left.length === 0 && right.length === 0)
|
|
48
|
+
return undefined;
|
|
49
|
+
if (left.length !== right.length)
|
|
50
|
+
return false;
|
|
51
|
+
return left.every((entry, index) => entry === right[index]);
|
|
52
|
+
}
|
|
53
|
+
function signature(node, out) {
|
|
54
|
+
const owners = node.provenance?.owners;
|
|
55
|
+
if (owners !== undefined && owners.length > 0) {
|
|
56
|
+
out.push(`${owners.map((owner) => owner.name).join('>')}|${wiringKey(node.wiring)}`);
|
|
57
|
+
}
|
|
58
|
+
for (const child of node.children)
|
|
59
|
+
signature(child, out);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Wiring as one comparable string.
|
|
64
|
+
*
|
|
65
|
+
* Field order is fixed here rather than taken from `Object.keys`, so that a
|
|
66
|
+
* wiring gaining a field it did not have changes the key for a reason a reader
|
|
67
|
+
* can name, and never because the two objects were built in a different order.
|
|
68
|
+
*/
|
|
69
|
+
function wiringKey(wiring) {
|
|
70
|
+
if (wiring === undefined)
|
|
71
|
+
return '';
|
|
72
|
+
return [
|
|
73
|
+
wiring.hooks?.join(',') ?? '',
|
|
74
|
+
wiring.wrappers?.join(',') ?? '',
|
|
75
|
+
wiring.contexts?.join(',') ?? '',
|
|
76
|
+
wiring.key ?? '',
|
|
77
|
+
].join(';');
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=slice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slice.js","sourceRoot":"","sources":["../../src/compare/slice.ts"],"names":[],"mappings":"AAqDA;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CACrB,IAAyB,EACzB,UAAiD,EACjD,KAAc;IAEd,2EAA2E;IAC3E,yEAAyE;IACzE,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;IAElE,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAEzE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,IAAI,IAAI,KAAK,KAAK;YAAE,OAAO,UAAU,CAAC;QACtC,OAAO,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;IACzC,CAAC;IAED,IAAI,MAAM;QAAE,OAAO,WAAW,CAAC;IAC/B,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,UAAU,CAAC;IACtC,6EAA6E;IAC7E,2EAA2E;IAC3E,uEAAuE;IACvE,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACxC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,QAAQ,CAAC,QAAsB,EAAE,SAAuB;IACtE,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,SAAS,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACvC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9D,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,SAAS,CAAC,IAAkB,EAAE,GAAa;IAClD,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC;IACvC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACvF,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ;QAAE,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACzD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,SAAS,SAAS,CAAC,MAA0B;IAC3C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACpC,OAAO;QACL,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAC7B,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAChC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAChC,MAAM,CAAC,GAAG,IAAI,EAAE;KACjB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC","sourcesContent":["import type { SemanticNode } from '../format/snapshot.js';\nimport type { Wiring } from '../format/wiring.js';\nimport type { PartedBoundary } from './parting.js';\n\n/**\n * What kind of parting this is, before anything about which one.\n *\n * The rungs in `parting.ts` answer *which input moved*. This answers the\n * question asked first and answered least: **is this worth looking at at all.**\n * Three facts are read independently — did the component tree move, did any\n * input move, did the output move — and the eight combinations collapse to\n * seven sentences, five of which are a triage decision on their own:\n *\n * | component tree | inputs | output | slice |\n * |---|---|---|---|\n * | same | same | **moved** | `flake` |\n * | any | **moved** | **moved** | `variation` |\n * | **moved** | same | **moved** | `reshaped` |\n * | **moved** | any | same | `refactor` |\n * | same | **moved** | same | `absorbed` |\n * | same | same | same | `settled` |\n * | unread | — | **moved** | `unread` |\n *\n * `reshaped` is the row that used to be missing. It was answered `variation`,\n * whose sentence says an input moved — and the whole point of this table is\n * that in that row no input did. Two readings whose component trees are not the\n * same tree, reached from inputs that all agreed, is a component that chose a\n * different shape: a branch taken differently between two arms, or a rewrite\n * between two revisions. Either way the tree is the finding and the rungs below\n * have nothing to add, because there is no moved input for them to name.\n *\n * `refactor` is the one that pays for the other six. A component tree that\n * moved while the page did not is the receipt a refactor never gets: the\n * screenshots match, which is what a pixel differ says and all it says, and\n * *the components underneath were rewritten*, which is the fact somebody wanted\n * confirmed before merging.\n */\nexport type PartingSlice =\n /** Nothing moved: not the tree, not an input, not the output. */\n | 'settled'\n /** An input moved and the output followed. The ordinary parting. */\n | 'variation'\n /** Every input agreed, the tree held, and the output moved anyway. */\n | 'flake'\n /** The component tree is a different tree, no input moved, and the output followed. */\n | 'reshaped'\n /** The component tree moved and the output did not. */\n | 'refactor'\n /** An input moved and the output did not: the component ignored it. */\n | 'absorbed'\n /** The output moved and what would explain it was not read. */\n | 'unread';\n\n/**\n * Decide the slice from three independent readings.\n *\n * `tree` is `undefined` when neither side carried provenance — not `false`.\n * A run that never asked what components were there has not found them equal,\n * and both `refactor` and `flake` are claims about the component tree that such\n * a run is not entitled to make (ADR-0002).\n */\nexport function sliceOf(\n tree: boolean | undefined,\n boundaries: readonly PartedBoundary[] | undefined,\n moved: boolean,\n): PartingSlice {\n // Nothing was read, so nothing may be alleged. `flake` in particular is an\n // accusation, and it is the one this project must never make on silence.\n if (boundaries === undefined) return moved ? 'unread' : 'settled';\n\n const inputs = boundaries.some((boundary) => boundary.inputs.length > 0);\n\n if (!moved) {\n if (tree === false) return 'refactor';\n return inputs ? 'absorbed' : 'settled';\n }\n\n if (inputs) return 'variation';\n if (tree === false) return 'reshaped';\n // `flake` is the accusation, and it rests on the component tree having held.\n // A run that read holdings but no provenance never established that, so it\n // gets the rung that says so rather than the one that blames the page.\n if (tree === undefined) return 'unread';\n return 'flake';\n}\n\n/**\n * Whether the two component trees are the same tree.\n *\n * The signature is the owner chain plus the wiring at each node that carries\n * one, in document order — the shape `wiringOf` already records, which is what\n * makes this a reading of the fiber rather than of the markup it produced. A\n * renamed component, an added `memo`, a hook inserted, a `key` changed and a\n * subtree moved all land here; a repainted pixel does not.\n *\n * `undefined` when neither side carried provenance at any node. A page read\n * without a framework adapter has no component tree to compare, which is a\n * different answer from two trees that matched.\n */\nexport function sameTree(baseline: SemanticNode, candidate: SemanticNode): boolean | undefined {\n const left = signature(baseline, []);\n const right = signature(candidate, []);\n if (left.length === 0 && right.length === 0) return undefined;\n if (left.length !== right.length) return false;\n return left.every((entry, index) => entry === right[index]);\n}\n\nfunction signature(node: SemanticNode, out: string[]): string[] {\n const owners = node.provenance?.owners;\n if (owners !== undefined && owners.length > 0) {\n out.push(`${owners.map((owner) => owner.name).join('>')}|${wiringKey(node.wiring)}`);\n }\n for (const child of node.children) signature(child, out);\n return out;\n}\n\n/**\n * Wiring as one comparable string.\n *\n * Field order is fixed here rather than taken from `Object.keys`, so that a\n * wiring gaining a field it did not have changes the key for a reason a reader\n * can name, and never because the two objects were built in a different order.\n */\nfunction wiringKey(wiring: Wiring | undefined): string {\n if (wiring === undefined) return '';\n return [\n wiring.hooks?.join(',') ?? '',\n wiring.wrappers?.join(',') ?? '',\n wiring.contexts?.join(',') ?? '',\n wiring.key ?? '',\n ].join(';');\n}\n"]}
|