@variance-authority/tribunal 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 +100 -0
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/dist/__fixtures__/review.d.ts +47 -0
- package/dist/__fixtures__/review.js +145 -0
- package/dist/__fixtures__/review.js.map +1 -0
- package/dist/bindings.d.ts +134 -0
- package/dist/bindings.js +88 -0
- package/dist/bindings.js.map +1 -0
- package/dist/changelog.d.ts +132 -0
- package/dist/changelog.js +129 -0
- package/dist/changelog.js.map +1 -0
- package/dist/history-queries.d.ts +82 -0
- package/dist/history-queries.js +200 -0
- package/dist/history-queries.js.map +1 -0
- package/dist/history-rows.d.ts +42 -0
- package/dist/history-rows.js +148 -0
- package/dist/history-rows.js.map +1 -0
- package/dist/history.d.ts +40 -0
- package/dist/history.js +186 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/migration-steps.d.ts +32 -0
- package/dist/migration-steps.js +303 -0
- package/dist/migration-steps.js.map +1 -0
- package/dist/migrations.d.ts +58 -0
- package/dist/migrations.js +283 -0
- package/dist/migrations.js.map +1 -0
- package/dist/next.d.ts +94 -0
- package/dist/next.js +44 -0
- package/dist/next.js.map +1 -0
- package/dist/node/bin.d.ts +120 -0
- package/dist/node/bin.js +337 -0
- package/dist/node/bin.js.map +1 -0
- package/dist/node/bucket.d.ts +58 -0
- package/dist/node/bucket.js +142 -0
- package/dist/node/bucket.js.map +1 -0
- package/dist/node/database.d.ts +33 -0
- package/dist/node/database.js +157 -0
- package/dist/node/database.js.map +1 -0
- package/dist/node/index.d.ts +43 -0
- package/dist/node/index.js +43 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/serve.d.ts +81 -0
- package/dist/node/serve.js +193 -0
- package/dist/node/serve.js.map +1 -0
- package/dist/node/ui-assets.d.ts +47 -0
- package/dist/node/ui-assets.js +60 -0
- package/dist/node/ui-assets.js.map +1 -0
- package/dist/review-ingest.d.ts +24 -0
- package/dist/review-ingest.js +184 -0
- package/dist/review-ingest.js.map +1 -0
- package/dist/review-read.d.ts +87 -0
- package/dist/review-read.js +361 -0
- package/dist/review-read.js.map +1 -0
- package/dist/review-rows.d.ts +30 -0
- package/dist/review-rows.js +67 -0
- package/dist/review-rows.js.map +1 -0
- package/dist/review-types.d.ts +414 -0
- package/dist/review-types.js +2 -0
- package/dist/review-types.js.map +1 -0
- package/dist/review-write.d.ts +35 -0
- package/dist/review-write.js +86 -0
- package/dist/review-write.js.map +1 -0
- package/dist/review.d.ts +53 -0
- package/dist/review.fixtures.d.ts +47 -0
- package/dist/review.fixtures.js +145 -0
- package/dist/review.fixtures.js.map +1 -0
- package/dist/review.js +245 -0
- package/dist/review.js.map +1 -0
- package/dist/schema.d.ts +36 -0
- package/dist/schema.js +39 -0
- package/dist/schema.js.map +1 -0
- package/dist/store.d.ts +58 -0
- package/dist/store.js +278 -0
- package/dist/store.js.map +1 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +92 -0
- package/dist/testing.js.map +1 -0
- package/dist/ui/__probe.d.ts +3 -0
- package/dist/ui/__probe.js +5 -0
- package/dist/ui/__probe.js.map +1 -0
- package/dist/ui/attribution.d.ts +46 -0
- package/dist/ui/attribution.js +79 -0
- package/dist/ui/attribution.js.map +1 -0
- package/dist/ui/because.d.ts +51 -0
- package/dist/ui/because.js +52 -0
- package/dist/ui/because.js.map +1 -0
- package/dist/ui/browser-entry.d.ts +2 -0
- package/dist/ui/browser-entry.js +34 -0
- package/dist/ui/browser-entry.js.map +1 -0
- package/dist/ui/builds.d.ts +43 -0
- package/dist/ui/builds.js +79 -0
- package/dist/ui/builds.js.map +1 -0
- package/dist/ui/carried.d.ts +73 -0
- package/dist/ui/carried.js +63 -0
- package/dist/ui/carried.js.map +1 -0
- package/dist/ui/cause.d.ts +73 -0
- package/dist/ui/cause.js +133 -0
- package/dist/ui/cause.js.map +1 -0
- package/dist/ui/change-story.d.ts +153 -0
- package/dist/ui/change-story.js +253 -0
- package/dist/ui/change-story.js.map +1 -0
- package/dist/ui/change.d.ts +51 -0
- package/dist/ui/change.js +125 -0
- package/dist/ui/change.js.map +1 -0
- package/dist/ui/client.d.ts +74 -0
- package/dist/ui/client.js +88 -0
- package/dist/ui/client.js.map +1 -0
- package/dist/ui/consumers.d.ts +80 -0
- package/dist/ui/consumers.js +125 -0
- package/dist/ui/consumers.js.map +1 -0
- package/dist/ui/control.d.ts +63 -0
- package/dist/ui/control.js +39 -0
- package/dist/ui/control.js.map +1 -0
- package/dist/ui/crossing.d.ts +55 -0
- package/dist/ui/crossing.js +74 -0
- package/dist/ui/crossing.js.map +1 -0
- package/dist/ui/declarations.d.ts +36 -0
- package/dist/ui/declarations.js +73 -0
- package/dist/ui/declarations.js.map +1 -0
- package/dist/ui/distance.d.ts +154 -0
- package/dist/ui/distance.js +180 -0
- package/dist/ui/distance.js.map +1 -0
- package/dist/ui/divergence.d.ts +40 -0
- package/dist/ui/divergence.js +176 -0
- package/dist/ui/divergence.js.map +1 -0
- package/dist/ui/docket.d.ts +39 -0
- package/dist/ui/docket.js +172 -0
- package/dist/ui/docket.js.map +1 -0
- package/dist/ui/findings.d.ts +42 -0
- package/dist/ui/findings.js +110 -0
- package/dist/ui/findings.js.map +1 -0
- package/dist/ui/foreseen.d.ts +55 -0
- package/dist/ui/foreseen.js +91 -0
- package/dist/ui/foreseen.js.map +1 -0
- package/dist/ui/glance.d.ts +101 -0
- package/dist/ui/glance.js +101 -0
- package/dist/ui/glance.js.map +1 -0
- package/dist/ui/grouping.d.ts +158 -0
- package/dist/ui/grouping.js +187 -0
- package/dist/ui/grouping.js.map +1 -0
- package/dist/ui/handed.d.ts +60 -0
- package/dist/ui/handed.js +122 -0
- package/dist/ui/handed.js.map +1 -0
- package/dist/ui/history.d.ts +88 -0
- package/dist/ui/history.js +170 -0
- package/dist/ui/history.js.map +1 -0
- package/dist/ui/holding.d.ts +84 -0
- package/dist/ui/holding.js +90 -0
- package/dist/ui/holding.js.map +1 -0
- package/dist/ui/impact.d.ts +31 -0
- package/dist/ui/impact.js +132 -0
- package/dist/ui/impact.js.map +1 -0
- package/dist/ui/index.d.ts +21 -0
- package/dist/ui/index.js +21 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/lead.d.ts +35 -0
- package/dist/ui/lead.js +34 -0
- package/dist/ui/lead.js.map +1 -0
- package/dist/ui/look.d.ts +109 -0
- package/dist/ui/look.js +248 -0
- package/dist/ui/look.js.map +1 -0
- package/dist/ui/mark.d.ts +11 -0
- package/dist/ui/mark.js +13 -0
- package/dist/ui/mark.js.map +1 -0
- package/dist/ui/moved.d.ts +121 -0
- package/dist/ui/moved.js +143 -0
- package/dist/ui/moved.js.map +1 -0
- package/dist/ui/navigation.d.ts +26 -0
- package/dist/ui/navigation.js +53 -0
- package/dist/ui/navigation.js.map +1 -0
- package/dist/ui/order.d.ts +113 -0
- package/dist/ui/order.js +226 -0
- package/dist/ui/order.js.map +1 -0
- package/dist/ui/origins.d.ts +42 -0
- package/dist/ui/origins.js +149 -0
- package/dist/ui/origins.js.map +1 -0
- package/dist/ui/outcome.d.ts +93 -0
- package/dist/ui/outcome.js +162 -0
- package/dist/ui/outcome.js.map +1 -0
- package/dist/ui/parted.d.ts +68 -0
- package/dist/ui/parted.js +101 -0
- package/dist/ui/parted.js.map +1 -0
- package/dist/ui/rail.d.ts +31 -0
- package/dist/ui/rail.js +21 -0
- package/dist/ui/rail.js.map +1 -0
- package/dist/ui/reach.d.ts +66 -0
- package/dist/ui/reach.js +185 -0
- package/dist/ui/reach.js.map +1 -0
- package/dist/ui/regions.d.ts +82 -0
- package/dist/ui/regions.js +67 -0
- package/dist/ui/regions.js.map +1 -0
- package/dist/ui/review.bundle.js +69 -0
- package/dist/ui/review.d.ts +97 -0
- package/dist/ui/review.js +39 -0
- package/dist/ui/review.js.map +1 -0
- package/dist/ui/root.d.ts +72 -0
- package/dist/ui/root.js +102 -0
- package/dist/ui/root.js.map +1 -0
- package/dist/ui/route.d.ts +95 -0
- package/dist/ui/route.js +114 -0
- package/dist/ui/route.js.map +1 -0
- package/dist/ui/run.d.ts +26 -0
- package/dist/ui/run.js +12 -0
- package/dist/ui/run.js.map +1 -0
- package/dist/ui/sense.d.ts +187 -0
- package/dist/ui/sense.js +222 -0
- package/dist/ui/sense.js.map +1 -0
- package/dist/ui/settled.d.ts +43 -0
- package/dist/ui/settled.js +59 -0
- package/dist/ui/settled.js.map +1 -0
- package/dist/ui/shell.d.ts +76 -0
- package/dist/ui/shell.js +65 -0
- package/dist/ui/shell.js.map +1 -0
- package/dist/ui/shift.d.ts +52 -0
- package/dist/ui/shift.js +98 -0
- package/dist/ui/shift.js.map +1 -0
- package/dist/ui/styles-docket.d.ts +18 -0
- package/dist/ui/styles-docket.js +281 -0
- package/dist/ui/styles-docket.js.map +1 -0
- package/dist/ui/styles-stage.d.ts +18 -0
- package/dist/ui/styles-stage.js +92 -0
- package/dist/ui/styles-stage.js.map +1 -0
- package/dist/ui/styles.d.ts +12 -0
- package/dist/ui/styles.js +482 -0
- package/dist/ui/styles.js.map +1 -0
- package/dist/ui/subject.d.ts +36 -0
- package/dist/ui/subject.js +138 -0
- package/dist/ui/subject.js.map +1 -0
- package/dist/ui/text.d.ts +129 -0
- package/dist/ui/text.js +164 -0
- package/dist/ui/text.js.map +1 -0
- package/dist/ui/variations.d.ts +37 -0
- package/dist/ui/variations.js +60 -0
- package/dist/ui/variations.js.map +1 -0
- package/dist/ui/viewer.d.ts +67 -0
- package/dist/ui/viewer.js +246 -0
- package/dist/ui/viewer.js.map +1 -0
- package/dist/worker-auth.d.ts +48 -0
- package/dist/worker-auth.js +74 -0
- package/dist/worker-auth.js.map +1 -0
- package/dist/worker-entry.d.ts +58 -0
- package/dist/worker-entry.js +36 -0
- package/dist/worker-entry.js.map +1 -0
- package/dist/worker-http.d.ts +39 -0
- package/dist/worker-http.js +111 -0
- package/dist/worker-http.js.map +1 -0
- package/dist/worker-input.d.ts +81 -0
- package/dist/worker-input.js +296 -0
- package/dist/worker-input.js.map +1 -0
- package/dist/worker.d.ts +91 -0
- package/dist/worker.js +287 -0
- package/dist/worker.js.map +1 -0
- package/mark.svg +30 -0
- package/migrations/0001_initial.sql +193 -0
- package/migrations/0002_changelog.sql +30 -0
- package/migrations/0003_accessibility.sql +8 -0
- package/migrations/0004_variations.sql +19 -0
- package/migrations/0005_reach.sql +26 -0
- package/migrations/0006_baseline-size.sql +7 -0
- package/migrations/0007_declarations.sql +7 -0
- package/migrations/0008_green-because.sql +7 -0
- package/migrations/0009_moved-bands.sql +6 -0
- package/migrations/0010_composition.sql +15 -0
- package/migrations/0011_movements.sql +21 -0
- package/migrations/0012_control-size.sql +6 -0
- package/package.json +110 -0
- package/wrangler.jsonc +45 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a reviewer should see first, derived rather than read out of a sentence.
|
|
3
|
+
*
|
|
4
|
+
* The line this replaces was the run's own prose: *2,015 pixels differ across 1
|
|
5
|
+
* region in Card, and the subject resized from 1280×394 to 1280×402; the
|
|
6
|
+
* collection of this subject reported ignore-unmatched (warn), so what was
|
|
7
|
+
* compared may be less than the whole subject.* Two hundred and twenty
|
|
8
|
+
* characters, three clauses, and the one thing a person would have noticed —
|
|
9
|
+
* **the card got eight pixels taller** — is in the middle of the second.
|
|
10
|
+
*
|
|
11
|
+
* It also names the wrong component. `describeChange` maps every region to its
|
|
12
|
+
* name and never reads `cause`, so a change that reflowed its container is
|
|
13
|
+
* reported under the container. On that subject `Card` is a passenger and
|
|
14
|
+
* `Button` is the only entry the hashes mark as a cause, in `geometry` **and**
|
|
15
|
+
* `token` — its own box and its own declared values both moved.
|
|
16
|
+
*
|
|
17
|
+
* ## The strip is three facts, and each is a measurement
|
|
18
|
+
*
|
|
19
|
+
* The cause, the growth, and the area. Nothing here is a sentence, because a
|
|
20
|
+
* sentence has to be read in order and these are answers to three questions
|
|
21
|
+
* asked at once — and because a reviewer who has read the same clause on twenty
|
|
22
|
+
* subjects stops reading it on the twenty-first.
|
|
23
|
+
*
|
|
24
|
+
* ## What it cannot say, and why that is not this module's fault
|
|
25
|
+
*
|
|
26
|
+
* The edit was `h-9 rounded-md px-3` to `h-11 rounded-lg px-5`: eight pixels of
|
|
27
|
+
* height, eight of horizontal padding, a larger radius. This can report the
|
|
28
|
+
* height, because a document's dimensions are recorded on both sides. It cannot
|
|
29
|
+
* report the padding, because a baseline keeps five digests per component and no
|
|
30
|
+
* values, so *what it was* is not on disk anywhere. That is a capture decision
|
|
31
|
+
* and it is the reason a band is called `token` rather than `--radius-md`.
|
|
32
|
+
*
|
|
33
|
+
* So the growth is printed as the growth, never as a property. A page that
|
|
34
|
+
* guessed *padding* from a height delta would be right on this subject and wrong
|
|
35
|
+
* on the first one that wrapped a line.
|
|
36
|
+
*
|
|
37
|
+
* What it can now say, since `ComponentHash.boxes`, is which control grew and by
|
|
38
|
+
* how much: `Button +36 × +8 px` beside the canvas's own `+8 px taller`. Those
|
|
39
|
+
* are two facts and the pair is the finding — the control got wider and the page
|
|
40
|
+
* did not, so the width was absorbed by a row that had the room and the height
|
|
41
|
+
* was not.
|
|
42
|
+
*/
|
|
43
|
+
import type { SubjectView } from '../review-types.js';
|
|
44
|
+
/** The component the record blames, and how the two tiers found it. */
|
|
45
|
+
export interface Blamed {
|
|
46
|
+
readonly component: string;
|
|
47
|
+
readonly bands: readonly string[];
|
|
48
|
+
/** A region in this render carries the name, so the picture found it too. */
|
|
49
|
+
readonly drawn: boolean;
|
|
50
|
+
/** Present on one side only — which `bands` alone cannot say. */
|
|
51
|
+
readonly presence?: 'added' | 'removed';
|
|
52
|
+
/**
|
|
53
|
+
* How much its own box grew, when the record measured it.
|
|
54
|
+
*
|
|
55
|
+
* The answer this page existed without. A band says *not what it was*; this
|
|
56
|
+
* says *thirty-six wider and eight taller*, which is the edit — and it is a
|
|
57
|
+
* measurement of the control rather than of the canvas, so it survives on a
|
|
58
|
+
* subject whose page height never moved.
|
|
59
|
+
*/
|
|
60
|
+
readonly grew?: {
|
|
61
|
+
readonly width: number;
|
|
62
|
+
readonly height: number;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** How the canvas itself moved, when it did. */
|
|
66
|
+
export interface Grew {
|
|
67
|
+
readonly axis: 'taller' | 'shorter' | 'wider' | 'narrower' | 'resized';
|
|
68
|
+
readonly by: number;
|
|
69
|
+
readonly from: {
|
|
70
|
+
readonly width: number;
|
|
71
|
+
readonly height: number;
|
|
72
|
+
};
|
|
73
|
+
readonly to: {
|
|
74
|
+
readonly width: number;
|
|
75
|
+
readonly height: number;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export interface Glance {
|
|
79
|
+
/**
|
|
80
|
+
* Every component whose own content moved, widest sense first.
|
|
81
|
+
*
|
|
82
|
+
* Empty is a real answer and not a missing one: the hashes were read and
|
|
83
|
+
* nothing owned the difference, which is what the picture is left to explain.
|
|
84
|
+
*/
|
|
85
|
+
readonly blamed: readonly Blamed[];
|
|
86
|
+
/** `false` when the baseline carried no hashes, so none of the above was asked. */
|
|
87
|
+
readonly measured: boolean;
|
|
88
|
+
readonly grew?: Grew;
|
|
89
|
+
readonly pixels: number;
|
|
90
|
+
readonly regions: number;
|
|
91
|
+
/**
|
|
92
|
+
* The component the largest region resolved to, when no blame names it.
|
|
93
|
+
*
|
|
94
|
+
* The container that reflowed. Printed as what it is — where the pixels
|
|
95
|
+
* landed — rather than as the subject of the change, which is the mistake the
|
|
96
|
+
* prose line makes.
|
|
97
|
+
*/
|
|
98
|
+
readonly landedIn?: string;
|
|
99
|
+
}
|
|
100
|
+
export declare function glanceOf(subject: SubjectView): Glance;
|
|
101
|
+
//# sourceMappingURL=glance.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a reviewer should see first, derived rather than read out of a sentence.
|
|
3
|
+
*
|
|
4
|
+
* The line this replaces was the run's own prose: *2,015 pixels differ across 1
|
|
5
|
+
* region in Card, and the subject resized from 1280×394 to 1280×402; the
|
|
6
|
+
* collection of this subject reported ignore-unmatched (warn), so what was
|
|
7
|
+
* compared may be less than the whole subject.* Two hundred and twenty
|
|
8
|
+
* characters, three clauses, and the one thing a person would have noticed —
|
|
9
|
+
* **the card got eight pixels taller** — is in the middle of the second.
|
|
10
|
+
*
|
|
11
|
+
* It also names the wrong component. `describeChange` maps every region to its
|
|
12
|
+
* name and never reads `cause`, so a change that reflowed its container is
|
|
13
|
+
* reported under the container. On that subject `Card` is a passenger and
|
|
14
|
+
* `Button` is the only entry the hashes mark as a cause, in `geometry` **and**
|
|
15
|
+
* `token` — its own box and its own declared values both moved.
|
|
16
|
+
*
|
|
17
|
+
* ## The strip is three facts, and each is a measurement
|
|
18
|
+
*
|
|
19
|
+
* The cause, the growth, and the area. Nothing here is a sentence, because a
|
|
20
|
+
* sentence has to be read in order and these are answers to three questions
|
|
21
|
+
* asked at once — and because a reviewer who has read the same clause on twenty
|
|
22
|
+
* subjects stops reading it on the twenty-first.
|
|
23
|
+
*
|
|
24
|
+
* ## What it cannot say, and why that is not this module's fault
|
|
25
|
+
*
|
|
26
|
+
* The edit was `h-9 rounded-md px-3` to `h-11 rounded-lg px-5`: eight pixels of
|
|
27
|
+
* height, eight of horizontal padding, a larger radius. This can report the
|
|
28
|
+
* height, because a document's dimensions are recorded on both sides. It cannot
|
|
29
|
+
* report the padding, because a baseline keeps five digests per component and no
|
|
30
|
+
* values, so *what it was* is not on disk anywhere. That is a capture decision
|
|
31
|
+
* and it is the reason a band is called `token` rather than `--radius-md`.
|
|
32
|
+
*
|
|
33
|
+
* So the growth is printed as the growth, never as a property. A page that
|
|
34
|
+
* guessed *padding* from a height delta would be right on this subject and wrong
|
|
35
|
+
* on the first one that wrapped a line.
|
|
36
|
+
*
|
|
37
|
+
* What it can now say, since `ComponentHash.boxes`, is which control grew and by
|
|
38
|
+
* how much: `Button +36 × +8 px` beside the canvas's own `+8 px taller`. Those
|
|
39
|
+
* are two facts and the pair is the finding — the control got wider and the page
|
|
40
|
+
* did not, so the width was absorbed by a row that had the room and the height
|
|
41
|
+
* was not.
|
|
42
|
+
*/
|
|
43
|
+
import { leadOf } from './lead.js';
|
|
44
|
+
export function glanceOf(subject) {
|
|
45
|
+
const moved = subject.moved;
|
|
46
|
+
const blamed = (moved ?? [])
|
|
47
|
+
.filter((entry) => entry.cause)
|
|
48
|
+
.map((entry) => ({
|
|
49
|
+
component: entry.component,
|
|
50
|
+
bands: entry.bands,
|
|
51
|
+
drawn: subject.regions.some((region) => region.component === entry.component),
|
|
52
|
+
...(entry.presence === undefined ? {} : { presence: entry.presence }),
|
|
53
|
+
...(entry.grew === undefined ? {} : { grew: entry.grew }),
|
|
54
|
+
}))
|
|
55
|
+
.sort((left, right) => right.bands.length - left.bands.length ||
|
|
56
|
+
left.component.localeCompare(right.component));
|
|
57
|
+
const lead = leadOf(subject) ?? subject.regions[0];
|
|
58
|
+
const landedIn = lead?.component !== undefined &&
|
|
59
|
+
!blamed.some((entry) => entry.component === lead.component)
|
|
60
|
+
? lead.component
|
|
61
|
+
: undefined;
|
|
62
|
+
const grew = grewBy(subject);
|
|
63
|
+
return {
|
|
64
|
+
blamed,
|
|
65
|
+
measured: moved !== undefined,
|
|
66
|
+
...(grew === undefined ? {} : { grew }),
|
|
67
|
+
pixels: subject.changedPixels,
|
|
68
|
+
regions: subject.regions.length,
|
|
69
|
+
...(landedIn === undefined ? {} : { landedIn }),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The canvas delta, named on the axis that moved.
|
|
74
|
+
*
|
|
75
|
+
* One axis is the ordinary case and the one worth a word: a component that got
|
|
76
|
+
* taller pushed everything under it down, and *8 px taller* is the sentence a
|
|
77
|
+
* reviewer would have written themselves. Both axes at once gets `resized` and
|
|
78
|
+
* the larger magnitude, because there is no single number to lead with and
|
|
79
|
+
* picking one would hide the other.
|
|
80
|
+
*/
|
|
81
|
+
function grewBy(subject) {
|
|
82
|
+
const to = subject.size;
|
|
83
|
+
const from = subject.baseline;
|
|
84
|
+
if (to === undefined || from === undefined)
|
|
85
|
+
return undefined;
|
|
86
|
+
const height = to.height - from.height;
|
|
87
|
+
const width = to.width - from.width;
|
|
88
|
+
if (height === 0 && width === 0)
|
|
89
|
+
return undefined;
|
|
90
|
+
const axis = height !== 0 && width !== 0
|
|
91
|
+
? 'resized'
|
|
92
|
+
: height > 0
|
|
93
|
+
? 'taller'
|
|
94
|
+
: height < 0
|
|
95
|
+
? 'shorter'
|
|
96
|
+
: width > 0
|
|
97
|
+
? 'wider'
|
|
98
|
+
: 'narrower';
|
|
99
|
+
return { axis, by: Math.max(Math.abs(height), Math.abs(width)), from, to };
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=glance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glance.js","sourceRoot":"","sources":["../../src/ui/glance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a reviewer should see first, derived rather than read out of a sentence.\n *\n * The line this replaces was the run's own prose: *2,015 pixels differ across 1\n * region in Card, and the subject resized from 1280×394 to 1280×402; the\n * collection of this subject reported ignore-unmatched (warn), so what was\n * compared may be less than the whole subject.* Two hundred and twenty\n * characters, three clauses, and the one thing a person would have noticed —\n * **the card got eight pixels taller** — is in the middle of the second.\n *\n * It also names the wrong component. `describeChange` maps every region to its\n * name and never reads `cause`, so a change that reflowed its container is\n * reported under the container. On that subject `Card` is a passenger and\n * `Button` is the only entry the hashes mark as a cause, in `geometry` **and**\n * `token` — its own box and its own declared values both moved.\n *\n * ## The strip is three facts, and each is a measurement\n *\n * The cause, the growth, and the area. Nothing here is a sentence, because a\n * sentence has to be read in order and these are answers to three questions\n * asked at once — and because a reviewer who has read the same clause on twenty\n * subjects stops reading it on the twenty-first.\n *\n * ## What it cannot say, and why that is not this module's fault\n *\n * The edit was `h-9 rounded-md px-3` to `h-11 rounded-lg px-5`: eight pixels of\n * height, eight of horizontal padding, a larger radius. This can report the\n * height, because a document's dimensions are recorded on both sides. It cannot\n * report the padding, because a baseline keeps five digests per component and no\n * values, so *what it was* is not on disk anywhere. That is a capture decision\n * and it is the reason a band is called `token` rather than `--radius-md`.\n *\n * So the growth is printed as the growth, never as a property. A page that\n * guessed *padding* from a height delta would be right on this subject and wrong\n * on the first one that wrapped a line.\n *\n * What it can now say, since `ComponentHash.boxes`, is which control grew and by\n * how much: `Button +36 × +8 px` beside the canvas's own `+8 px taller`. Those\n * are two facts and the pair is the finding — the control got wider and the page\n * did not, so the width was absorbed by a row that had the room and the height\n * was not.\n */\n\nimport type { SubjectView } from '../review-types.js';\nimport { leadOf } from './lead.js';\n\n/** The component the record blames, and how the two tiers found it. */\nexport interface Blamed {\n readonly component: string;\n readonly bands: readonly string[];\n /** A region in this render carries the name, so the picture found it too. */\n readonly drawn: boolean;\n /** Present on one side only — which `bands` alone cannot say. */\n readonly presence?: 'added' | 'removed';\n /**\n * How much its own box grew, when the record measured it.\n *\n * The answer this page existed without. A band says *not what it was*; this\n * says *thirty-six wider and eight taller*, which is the edit — and it is a\n * measurement of the control rather than of the canvas, so it survives on a\n * subject whose page height never moved.\n */\n readonly grew?: { readonly width: number; readonly height: number };\n}\n\n/** How the canvas itself moved, when it did. */\nexport interface Grew {\n readonly axis: 'taller' | 'shorter' | 'wider' | 'narrower' | 'resized';\n readonly by: number;\n readonly from: { readonly width: number; readonly height: number };\n readonly to: { readonly width: number; readonly height: number };\n}\n\nexport interface Glance {\n /**\n * Every component whose own content moved, widest sense first.\n *\n * Empty is a real answer and not a missing one: the hashes were read and\n * nothing owned the difference, which is what the picture is left to explain.\n */\n readonly blamed: readonly Blamed[];\n /** `false` when the baseline carried no hashes, so none of the above was asked. */\n readonly measured: boolean;\n readonly grew?: Grew;\n readonly pixels: number;\n readonly regions: number;\n /**\n * The component the largest region resolved to, when no blame names it.\n *\n * The container that reflowed. Printed as what it is — where the pixels\n * landed — rather than as the subject of the change, which is the mistake the\n * prose line makes.\n */\n readonly landedIn?: string;\n}\n\nexport function glanceOf(subject: SubjectView): Glance {\n const moved = subject.moved;\n const blamed = (moved ?? [])\n .filter((entry) => entry.cause)\n .map((entry) => ({\n component: entry.component,\n bands: entry.bands,\n drawn: subject.regions.some((region) => region.component === entry.component),\n ...(entry.presence === undefined ? {} : { presence: entry.presence }),\n ...(entry.grew === undefined ? {} : { grew: entry.grew }),\n }))\n .sort((left, right) => right.bands.length - left.bands.length ||\n left.component.localeCompare(right.component));\n\n const lead = leadOf(subject) ?? subject.regions[0];\n const landedIn =\n lead?.component !== undefined &&\n !blamed.some((entry) => entry.component === lead.component)\n ? lead.component\n : undefined;\n\n const grew = grewBy(subject);\n\n return {\n blamed,\n measured: moved !== undefined,\n ...(grew === undefined ? {} : { grew }),\n pixels: subject.changedPixels,\n regions: subject.regions.length,\n ...(landedIn === undefined ? {} : { landedIn }),\n };\n}\n\n/**\n * The canvas delta, named on the axis that moved.\n *\n * One axis is the ordinary case and the one worth a word: a component that got\n * taller pushed everything under it down, and *8 px taller* is the sentence a\n * reviewer would have written themselves. Both axes at once gets `resized` and\n * the larger magnitude, because there is no single number to lead with and\n * picking one would hide the other.\n */\nfunction grewBy(subject: SubjectView): Grew | undefined {\n const to = subject.size;\n const from = subject.baseline;\n if (to === undefined || from === undefined) return undefined;\n\n const height = to.height - from.height;\n const width = to.width - from.width;\n if (height === 0 && width === 0) return undefined;\n\n const axis =\n height !== 0 && width !== 0\n ? 'resized'\n : height > 0\n ? 'taller'\n : height < 0\n ? 'shorter'\n : width > 0\n ? 'wider'\n : 'narrower';\n\n return { axis, by: Math.max(Math.abs(height), Math.abs(width)), from, to };\n}\n"]}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The changes, grouped by what caused them — which is the unit a reviewer acts on.
|
|
3
|
+
*
|
|
4
|
+
* Nobody approves a screenshot. A person looking at twenty changed renders is
|
|
5
|
+
* looking at *one* edit to `Button` twenty times, and the category ships that as
|
|
6
|
+
* twenty decisions: twenty thumbnails, twenty approve buttons, and no way to say
|
|
7
|
+
* the thing that is actually true — *yes, I meant to restyle the button, and yes,
|
|
8
|
+
* every card that grew by two millimetres grew because of it.*
|
|
9
|
+
*
|
|
10
|
+
* So the review item here is the origin, and a subject is a place it showed up.
|
|
11
|
+
*
|
|
12
|
+
* ## Three records meet on one group, and none of them can carry it alone
|
|
13
|
+
*
|
|
14
|
+
* - **The diff** says the commit reaches this component, and by what chain. An
|
|
15
|
+
* origin the commit does not reach is the strongest signal on the page: nothing
|
|
16
|
+
* you wrote arrives here and it moved anyway.
|
|
17
|
+
* - **The fingerprint** says whether they moved *the same way* — the shape with
|
|
18
|
+
* position and values removed. It does not decide the group and must not: a
|
|
19
|
+
* shape is a pixel digest, so one restyle lands as one shape on the buttons
|
|
20
|
+
* that are the same size and another on the one that is not. It answers the
|
|
21
|
+
* question after the group, which is which of these differences recur — and a
|
|
22
|
+
* shared shape is the set `variance accept --shape` takes.
|
|
23
|
+
* - **The record** says whether this component moves all the time or has been
|
|
24
|
+
* still since March. A 2px shift in a component that has caused an approved
|
|
25
|
+
* change in nineteen of twenty runs is a different decision from the same 2px in
|
|
26
|
+
* one that has caused none.
|
|
27
|
+
*
|
|
28
|
+
* ## What is not claimed
|
|
29
|
+
*
|
|
30
|
+
* Collateral is **not** attributed to an origin. Deciding which edit pushed which
|
|
31
|
+
* box around is exactly the attribution the semantic tier declined to make, and a
|
|
32
|
+
* group that swallowed it would be inventing the one number nobody measured. What
|
|
33
|
+
* a member carries instead is the list of components that moved *with* it in that
|
|
34
|
+
* subject, which is an observation rather than a claim.
|
|
35
|
+
*
|
|
36
|
+
* Apart from [`origins.tsx`](./origins.tsx), which draws all of this: the
|
|
37
|
+
* question *what is a change* is decided here and answered in one place, and the
|
|
38
|
+
* panel that renders it is long enough on its own.
|
|
39
|
+
*/
|
|
40
|
+
import type { BuildDetail, MovementView, SubjectView } from '../review-types.js';
|
|
41
|
+
import { type Rung } from './attribution.js';
|
|
42
|
+
/** One subject an origin showed up in. */
|
|
43
|
+
export interface Appearance {
|
|
44
|
+
readonly subject: SubjectView;
|
|
45
|
+
/** Pixels the origin's own cause region moved here. */
|
|
46
|
+
readonly pixels: number;
|
|
47
|
+
/** The shape of that region, when the run recorded one. */
|
|
48
|
+
readonly shape?: string;
|
|
49
|
+
/** Other components with regions in this subject — observed, not attributed. */
|
|
50
|
+
readonly alongside: readonly string[];
|
|
51
|
+
/**
|
|
52
|
+
* Why the run says it moved *here*, when the run said.
|
|
53
|
+
*
|
|
54
|
+
* Per appearance rather than per origin, because that is the grain the answer
|
|
55
|
+
* has: one `Button` is `edited` on the page whose file the diff names and
|
|
56
|
+
* `upstream` on the page where an edited parent hands it a different label.
|
|
57
|
+
*/
|
|
58
|
+
readonly movement?: MovementView;
|
|
59
|
+
}
|
|
60
|
+
export interface Origin {
|
|
61
|
+
readonly component: string;
|
|
62
|
+
/** Where the component is declared, from the build's own docket. */
|
|
63
|
+
readonly file?: string;
|
|
64
|
+
readonly pixels: number;
|
|
65
|
+
readonly appearances: readonly Appearance[];
|
|
66
|
+
/** How the commit arrives at this component, when a diff was read. */
|
|
67
|
+
readonly trail?: readonly string[];
|
|
68
|
+
/** `undefined` when the run carried no diff, which is not the same as `false`. */
|
|
69
|
+
readonly reached?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The rung the run put this component on, folded over its appearances.
|
|
72
|
+
*
|
|
73
|
+
* `undefined` is *nothing is on record* and never a sixth rung. It is what a
|
|
74
|
+
* build ingested before attributions were carried answers for everything, and
|
|
75
|
+
* a band that read it as `unexplained` would print a missing input as the
|
|
76
|
+
* loudest finding on the page.
|
|
77
|
+
*/
|
|
78
|
+
readonly cause?: Rung;
|
|
79
|
+
/**
|
|
80
|
+
* What the commit *does* reach in the renders this origin showed up in.
|
|
81
|
+
*
|
|
82
|
+
* The same relation as `SubjectReach.through`, unioned over this origin's
|
|
83
|
+
* appearances, and it is here for the case `reached === false` cannot describe
|
|
84
|
+
* on its own. A component the diff does not reach, moving in a render the diff
|
|
85
|
+
* reaches through three other components, is an ordinary Tuesday — most of a
|
|
86
|
+
* page belongs to libraries the import graph was never asked about. Without
|
|
87
|
+
* this the panel had one sentence for that and for a genuine orphan, and it
|
|
88
|
+
* spent it on the orphan.
|
|
89
|
+
*/
|
|
90
|
+
readonly through?: readonly string[];
|
|
91
|
+
/**
|
|
92
|
+
* Renders this origin showed up in that the commit reaches nothing in at all.
|
|
93
|
+
*
|
|
94
|
+
* Empty is the common case and the whole point of separating it: a difference
|
|
95
|
+
* with no path from the diff to any component in the render is the finding
|
|
96
|
+
* worth waking somebody for, and it is worth nothing if it is printed over
|
|
97
|
+
* every third-party component on the page as well.
|
|
98
|
+
*/
|
|
99
|
+
readonly stranded?: readonly string[];
|
|
100
|
+
/**
|
|
101
|
+
* Renders this origin showed up in that the reach record says nothing about.
|
|
102
|
+
*
|
|
103
|
+
* Split out of {@link Origin.stranded}, and the split is not pedantry. A report
|
|
104
|
+
* that carried component-level reach and no per-subject block put every one of
|
|
105
|
+
* its renders in the unreached pile, and the panel spent its loudest sentence —
|
|
106
|
+
* *the commit reaches nothing at all in this render* — on the ordinary fact
|
|
107
|
+
* that nobody wrote the row down. Absent is not empty.
|
|
108
|
+
*/
|
|
109
|
+
readonly unlisted?: readonly string[];
|
|
110
|
+
}
|
|
111
|
+
export interface Origins {
|
|
112
|
+
readonly origins: readonly Origin[];
|
|
113
|
+
/**
|
|
114
|
+
* Changed subjects no component claimed.
|
|
115
|
+
*
|
|
116
|
+
* Their own bucket, never folded into an origin. A subject that moved with
|
|
117
|
+
* nothing named as its cause is the one case where a reviewer has to open the
|
|
118
|
+
* picture, and hiding it inside a group would hand them somebody else's edit to
|
|
119
|
+
* approve it under.
|
|
120
|
+
*/
|
|
121
|
+
readonly unattributed: readonly SubjectView[];
|
|
122
|
+
}
|
|
123
|
+
export declare function originsOf(build: BuildDetail): Origins;
|
|
124
|
+
/**
|
|
125
|
+
* The names the run put on this change's cause, for the row to print.
|
|
126
|
+
*
|
|
127
|
+
* The rail used to carry a paragraph per band saying what the band meant — *a
|
|
128
|
+
* component the commit did edit draws each one and hands it what it renders* —
|
|
129
|
+
* while the name of the component that did the drawing sat in the record,
|
|
130
|
+
* unprinted, and the file a component is declared in sat in a `title` attribute
|
|
131
|
+
* nobody hovers. That is backwards twice: the definition is identical on every
|
|
132
|
+
* build, and the name is the only part that is about this one.
|
|
133
|
+
*
|
|
134
|
+
* So the paragraph is gone and this stands where it stood. Per rung, because
|
|
135
|
+
* each rung recorded a different kind of evidence and there is no general one:
|
|
136
|
+
* `edited` has the file the diff named, `token` has the custom properties that
|
|
137
|
+
* took new values, `upstream` has the edited component that reaches this one and
|
|
138
|
+
* the chain it reaches through.
|
|
139
|
+
*
|
|
140
|
+
* Folded over the appearances rather than read off the first, and the difference
|
|
141
|
+
* is the case worth having. One `CardFooter` can be handed its change by
|
|
142
|
+
* `ProductCard` on the product page and by `CartCard` on the cart, and a row
|
|
143
|
+
* printing whichever render came back first would name a page the reviewer is
|
|
144
|
+
* not looking at.
|
|
145
|
+
*/
|
|
146
|
+
export declare function sourceOf(origin: Origin): readonly string[];
|
|
147
|
+
/**
|
|
148
|
+
* What the differences look like, which is the half a component name cannot say.
|
|
149
|
+
*
|
|
150
|
+
* A component groups *what was edited*. A shape groups *what the edit did*, and
|
|
151
|
+
* the two answer different questions: eleven appearances under `Button` with one
|
|
152
|
+
* shape between them is a token that moved every button identically, and eleven
|
|
153
|
+
* with nine shapes is a component whose renders each absorbed the change
|
|
154
|
+
* differently. Both are one review. Only the first has a name a reviewer can
|
|
155
|
+
* carry to another build.
|
|
156
|
+
*/
|
|
157
|
+
export declare function shapesOf(appearances: readonly Appearance[]): Map<string, number>;
|
|
158
|
+
//# sourceMappingURL=grouping.d.ts.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The changes, grouped by what caused them — which is the unit a reviewer acts on.
|
|
3
|
+
*
|
|
4
|
+
* Nobody approves a screenshot. A person looking at twenty changed renders is
|
|
5
|
+
* looking at *one* edit to `Button` twenty times, and the category ships that as
|
|
6
|
+
* twenty decisions: twenty thumbnails, twenty approve buttons, and no way to say
|
|
7
|
+
* the thing that is actually true — *yes, I meant to restyle the button, and yes,
|
|
8
|
+
* every card that grew by two millimetres grew because of it.*
|
|
9
|
+
*
|
|
10
|
+
* So the review item here is the origin, and a subject is a place it showed up.
|
|
11
|
+
*
|
|
12
|
+
* ## Three records meet on one group, and none of them can carry it alone
|
|
13
|
+
*
|
|
14
|
+
* - **The diff** says the commit reaches this component, and by what chain. An
|
|
15
|
+
* origin the commit does not reach is the strongest signal on the page: nothing
|
|
16
|
+
* you wrote arrives here and it moved anyway.
|
|
17
|
+
* - **The fingerprint** says whether they moved *the same way* — the shape with
|
|
18
|
+
* position and values removed. It does not decide the group and must not: a
|
|
19
|
+
* shape is a pixel digest, so one restyle lands as one shape on the buttons
|
|
20
|
+
* that are the same size and another on the one that is not. It answers the
|
|
21
|
+
* question after the group, which is which of these differences recur — and a
|
|
22
|
+
* shared shape is the set `variance accept --shape` takes.
|
|
23
|
+
* - **The record** says whether this component moves all the time or has been
|
|
24
|
+
* still since March. A 2px shift in a component that has caused an approved
|
|
25
|
+
* change in nineteen of twenty runs is a different decision from the same 2px in
|
|
26
|
+
* one that has caused none.
|
|
27
|
+
*
|
|
28
|
+
* ## What is not claimed
|
|
29
|
+
*
|
|
30
|
+
* Collateral is **not** attributed to an origin. Deciding which edit pushed which
|
|
31
|
+
* box around is exactly the attribution the semantic tier declined to make, and a
|
|
32
|
+
* group that swallowed it would be inventing the one number nobody measured. What
|
|
33
|
+
* a member carries instead is the list of components that moved *with* it in that
|
|
34
|
+
* subject, which is an observation rather than a claim.
|
|
35
|
+
*
|
|
36
|
+
* Apart from [`origins.tsx`](./origins.tsx), which draws all of this: the
|
|
37
|
+
* question *what is a change* is decided here and answered in one place, and the
|
|
38
|
+
* panel that renders it is long enough on its own.
|
|
39
|
+
*/
|
|
40
|
+
import { attributionsOf, rungAcross } from './attribution.js';
|
|
41
|
+
import { leadOf } from './lead.js';
|
|
42
|
+
export function originsOf(build) {
|
|
43
|
+
const files = new Map(build.causes.map((cause) => [cause.component, cause.file]));
|
|
44
|
+
const attributed = attributionsOf(build);
|
|
45
|
+
const groups = new Map();
|
|
46
|
+
const unattributed = [];
|
|
47
|
+
for (const subject of build.subjects) {
|
|
48
|
+
if (subject.verdict !== 'changed')
|
|
49
|
+
continue;
|
|
50
|
+
const lead = leadOf(subject);
|
|
51
|
+
if (lead?.component === undefined) {
|
|
52
|
+
unattributed.push(subject);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const group = groups.get(lead.component) ?? { pixels: 0, where: [] };
|
|
56
|
+
group.pixels += lead.pixels;
|
|
57
|
+
const movement = attributed.at(lead.component, subject.subject);
|
|
58
|
+
group.where.push({
|
|
59
|
+
subject,
|
|
60
|
+
pixels: lead.pixels,
|
|
61
|
+
...(lead.fingerprint === undefined ? {} : { shape: lead.fingerprint }),
|
|
62
|
+
...(movement === undefined ? {} : { movement }),
|
|
63
|
+
alongside: [
|
|
64
|
+
...new Set(subject.regions
|
|
65
|
+
.map((region) => region.component)
|
|
66
|
+
.filter((name) => name !== undefined && name !== lead.component)),
|
|
67
|
+
].sort(),
|
|
68
|
+
});
|
|
69
|
+
groups.set(lead.component, group);
|
|
70
|
+
}
|
|
71
|
+
const reach = build.reach !== null && build.reach.whole === undefined ? build.reach : null;
|
|
72
|
+
const origins = [...groups.entries()]
|
|
73
|
+
.map(([component, group]) => {
|
|
74
|
+
const file = files.get(component);
|
|
75
|
+
const entry = reach?.components.find((each) => each.component === component);
|
|
76
|
+
// Folded over the renders this origin showed up in, not over every render
|
|
77
|
+
// in the build. A component that also moved somewhere this docket does not
|
|
78
|
+
// group it under moved there for its own reason, and letting that vote
|
|
79
|
+
// would band this change by a page the reviewer is not looking at.
|
|
80
|
+
const cause = rungAcross(group.where.flatMap((each) => each.movement ?? []));
|
|
81
|
+
return {
|
|
82
|
+
component,
|
|
83
|
+
pixels: group.pixels,
|
|
84
|
+
appearances: group.where,
|
|
85
|
+
...(cause === undefined ? {} : { cause }),
|
|
86
|
+
...(file === undefined ? {} : { file }),
|
|
87
|
+
...(reach === null ? {} : { reached: entry !== undefined, ...around(reach, group.where) }),
|
|
88
|
+
...(entry === undefined ? {} : { trail: entry.trail }),
|
|
89
|
+
};
|
|
90
|
+
})
|
|
91
|
+
.sort((left, right) => right.pixels === left.pixels
|
|
92
|
+
? left.component.localeCompare(right.component)
|
|
93
|
+
: right.pixels - left.pixels);
|
|
94
|
+
return { origins, unattributed };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* What the diff reaches around this origin, and where it reaches nothing.
|
|
98
|
+
*
|
|
99
|
+
* Read per appearance and then unioned, rather than asked of the component: the
|
|
100
|
+
* question is not *does the commit know this name* but *does the commit arrive in
|
|
101
|
+
* the pictures where this name moved*, and those have different answers exactly
|
|
102
|
+
* when the answer matters.
|
|
103
|
+
*
|
|
104
|
+
* Three piles, not two. A render the reach record has no row for is not a render
|
|
105
|
+
* the commit fails to reach — it is a render nobody asked about — and folding
|
|
106
|
+
* those together is how a report with no per-subject block ends up printing the
|
|
107
|
+
* page's loudest sentence over every change in the build.
|
|
108
|
+
*/
|
|
109
|
+
function around(reach, appearances) {
|
|
110
|
+
const through = new Set();
|
|
111
|
+
const stranded = [];
|
|
112
|
+
const unlisted = [];
|
|
113
|
+
for (const { subject } of appearances) {
|
|
114
|
+
const entry = reach.subjects?.[subject.subject];
|
|
115
|
+
if (entry === undefined)
|
|
116
|
+
unlisted.push(subject.subject);
|
|
117
|
+
else if (entry.reached)
|
|
118
|
+
for (const name of entry.through)
|
|
119
|
+
through.add(name);
|
|
120
|
+
else
|
|
121
|
+
stranded.push(subject.subject);
|
|
122
|
+
}
|
|
123
|
+
return { through: [...through].sort(), stranded, unlisted };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The names the run put on this change's cause, for the row to print.
|
|
127
|
+
*
|
|
128
|
+
* The rail used to carry a paragraph per band saying what the band meant — *a
|
|
129
|
+
* component the commit did edit draws each one and hands it what it renders* —
|
|
130
|
+
* while the name of the component that did the drawing sat in the record,
|
|
131
|
+
* unprinted, and the file a component is declared in sat in a `title` attribute
|
|
132
|
+
* nobody hovers. That is backwards twice: the definition is identical on every
|
|
133
|
+
* build, and the name is the only part that is about this one.
|
|
134
|
+
*
|
|
135
|
+
* So the paragraph is gone and this stands where it stood. Per rung, because
|
|
136
|
+
* each rung recorded a different kind of evidence and there is no general one:
|
|
137
|
+
* `edited` has the file the diff named, `token` has the custom properties that
|
|
138
|
+
* took new values, `upstream` has the edited component that reaches this one and
|
|
139
|
+
* the chain it reaches through.
|
|
140
|
+
*
|
|
141
|
+
* Folded over the appearances rather than read off the first, and the difference
|
|
142
|
+
* is the case worth having. One `CardFooter` can be handed its change by
|
|
143
|
+
* `ProductCard` on the product page and by `CartCard` on the cart, and a row
|
|
144
|
+
* printing whichever render came back first would name a page the reviewer is
|
|
145
|
+
* not looking at.
|
|
146
|
+
*/
|
|
147
|
+
export function sourceOf(origin) {
|
|
148
|
+
const found = new Set();
|
|
149
|
+
for (const { movement } of origin.appearances) {
|
|
150
|
+
if (movement === undefined)
|
|
151
|
+
continue;
|
|
152
|
+
if (movement.cause === 'edited' && movement.file !== undefined)
|
|
153
|
+
found.add(movement.file);
|
|
154
|
+
if (movement.cause === 'token')
|
|
155
|
+
for (const token of movement.tokens ?? [])
|
|
156
|
+
found.add(token);
|
|
157
|
+
if (movement.cause === 'upstream' && movement.upstream !== undefined) {
|
|
158
|
+
found.add([movement.upstream, ...(movement.through ?? [])].join(' → '));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Where the component is declared, when no movement named anything — which is
|
|
162
|
+
// most of what the `title` attribute was hiding, and is the answer a reviewer
|
|
163
|
+
// opening an unattributed row is about to go looking for anyway.
|
|
164
|
+
if (found.size === 0 && origin.file !== undefined)
|
|
165
|
+
found.add(origin.file);
|
|
166
|
+
return [...found].sort();
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* What the differences look like, which is the half a component name cannot say.
|
|
170
|
+
*
|
|
171
|
+
* A component groups *what was edited*. A shape groups *what the edit did*, and
|
|
172
|
+
* the two answer different questions: eleven appearances under `Button` with one
|
|
173
|
+
* shape between them is a token that moved every button identically, and eleven
|
|
174
|
+
* with nine shapes is a component whose renders each absorbed the change
|
|
175
|
+
* differently. Both are one review. Only the first has a name a reviewer can
|
|
176
|
+
* carry to another build.
|
|
177
|
+
*/
|
|
178
|
+
export function shapesOf(appearances) {
|
|
179
|
+
const clusters = new Map();
|
|
180
|
+
for (const each of appearances) {
|
|
181
|
+
if (each.shape === undefined)
|
|
182
|
+
continue;
|
|
183
|
+
clusters.set(each.shape, (clusters.get(each.shape) ?? 0) + 1);
|
|
184
|
+
}
|
|
185
|
+
return clusters;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=grouping.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grouping.js","sourceRoot":"","sources":["../../src/ui/grouping.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The changes, grouped by what caused them — which is the unit a reviewer acts on.\n *\n * Nobody approves a screenshot. A person looking at twenty changed renders is\n * looking at *one* edit to `Button` twenty times, and the category ships that as\n * twenty decisions: twenty thumbnails, twenty approve buttons, and no way to say\n * the thing that is actually true — *yes, I meant to restyle the button, and yes,\n * every card that grew by two millimetres grew because of it.*\n *\n * So the review item here is the origin, and a subject is a place it showed up.\n *\n * ## Three records meet on one group, and none of them can carry it alone\n *\n * - **The diff** says the commit reaches this component, and by what chain. An\n * origin the commit does not reach is the strongest signal on the page: nothing\n * you wrote arrives here and it moved anyway.\n * - **The fingerprint** says whether they moved *the same way* — the shape with\n * position and values removed. It does not decide the group and must not: a\n * shape is a pixel digest, so one restyle lands as one shape on the buttons\n * that are the same size and another on the one that is not. It answers the\n * question after the group, which is which of these differences recur — and a\n * shared shape is the set `variance accept --shape` takes.\n * - **The record** says whether this component moves all the time or has been\n * still since March. A 2px shift in a component that has caused an approved\n * change in nineteen of twenty runs is a different decision from the same 2px in\n * one that has caused none.\n *\n * ## What is not claimed\n *\n * Collateral is **not** attributed to an origin. Deciding which edit pushed which\n * box around is exactly the attribution the semantic tier declined to make, and a\n * group that swallowed it would be inventing the one number nobody measured. What\n * a member carries instead is the list of components that moved *with* it in that\n * subject, which is an observation rather than a claim.\n *\n * Apart from [`origins.tsx`](./origins.tsx), which draws all of this: the\n * question *what is a change* is decided here and answered in one place, and the\n * panel that renders it is long enough on its own.\n */\n\nimport type { BuildDetail, MovementView, SubjectView } from '../review-types.js';\nimport { attributionsOf, rungAcross, type Rung } from './attribution.js';\nimport { leadOf } from './lead.js';\n\n/** One subject an origin showed up in. */\nexport interface Appearance {\n readonly subject: SubjectView;\n /** Pixels the origin's own cause region moved here. */\n readonly pixels: number;\n /** The shape of that region, when the run recorded one. */\n readonly shape?: string;\n /** Other components with regions in this subject — observed, not attributed. */\n readonly alongside: readonly string[];\n /**\n * Why the run says it moved *here*, when the run said.\n *\n * Per appearance rather than per origin, because that is the grain the answer\n * has: one `Button` is `edited` on the page whose file the diff names and\n * `upstream` on the page where an edited parent hands it a different label.\n */\n readonly movement?: MovementView;\n}\n\nexport interface Origin {\n readonly component: string;\n /** Where the component is declared, from the build's own docket. */\n readonly file?: string;\n readonly pixels: number;\n readonly appearances: readonly Appearance[];\n /** How the commit arrives at this component, when a diff was read. */\n readonly trail?: readonly string[];\n /** `undefined` when the run carried no diff, which is not the same as `false`. */\n readonly reached?: boolean;\n /**\n * The rung the run put this component on, folded over its appearances.\n *\n * `undefined` is *nothing is on record* and never a sixth rung. It is what a\n * build ingested before attributions were carried answers for everything, and\n * a band that read it as `unexplained` would print a missing input as the\n * loudest finding on the page.\n */\n readonly cause?: Rung;\n /**\n * What the commit *does* reach in the renders this origin showed up in.\n *\n * The same relation as `SubjectReach.through`, unioned over this origin's\n * appearances, and it is here for the case `reached === false` cannot describe\n * on its own. A component the diff does not reach, moving in a render the diff\n * reaches through three other components, is an ordinary Tuesday — most of a\n * page belongs to libraries the import graph was never asked about. Without\n * this the panel had one sentence for that and for a genuine orphan, and it\n * spent it on the orphan.\n */\n readonly through?: readonly string[];\n /**\n * Renders this origin showed up in that the commit reaches nothing in at all.\n *\n * Empty is the common case and the whole point of separating it: a difference\n * with no path from the diff to any component in the render is the finding\n * worth waking somebody for, and it is worth nothing if it is printed over\n * every third-party component on the page as well.\n */\n readonly stranded?: readonly string[];\n /**\n * Renders this origin showed up in that the reach record says nothing about.\n *\n * Split out of {@link Origin.stranded}, and the split is not pedantry. A report\n * that carried component-level reach and no per-subject block put every one of\n * its renders in the unreached pile, and the panel spent its loudest sentence —\n * *the commit reaches nothing at all in this render* — on the ordinary fact\n * that nobody wrote the row down. Absent is not empty.\n */\n readonly unlisted?: readonly string[];\n}\n\nexport interface Origins {\n readonly origins: readonly Origin[];\n /**\n * Changed subjects no component claimed.\n *\n * Their own bucket, never folded into an origin. A subject that moved with\n * nothing named as its cause is the one case where a reviewer has to open the\n * picture, and hiding it inside a group would hand them somebody else's edit to\n * approve it under.\n */\n readonly unattributed: readonly SubjectView[];\n}\n\nexport function originsOf(build: BuildDetail): Origins {\n const files = new Map(build.causes.map((cause) => [cause.component, cause.file]));\n const attributed = attributionsOf(build);\n const groups = new Map<string, { pixels: number; where: Appearance[] }>();\n const unattributed: SubjectView[] = [];\n\n for (const subject of build.subjects) {\n if (subject.verdict !== 'changed') continue;\n\n const lead = leadOf(subject);\n if (lead?.component === undefined) {\n unattributed.push(subject);\n continue;\n }\n\n const group = groups.get(lead.component) ?? { pixels: 0, where: [] };\n group.pixels += lead.pixels;\n const movement = attributed.at(lead.component, subject.subject);\n group.where.push({\n subject,\n pixels: lead.pixels,\n ...(lead.fingerprint === undefined ? {} : { shape: lead.fingerprint }),\n ...(movement === undefined ? {} : { movement }),\n alongside: [\n ...new Set(\n subject.regions\n .map((region) => region.component)\n .filter((name): name is string => name !== undefined && name !== lead.component),\n ),\n ].sort(),\n });\n groups.set(lead.component, group);\n }\n\n const reach = build.reach !== null && build.reach.whole === undefined ? build.reach : null;\n\n const origins = [...groups.entries()]\n .map(([component, group]): Origin => {\n const file = files.get(component);\n const entry = reach?.components.find((each) => each.component === component);\n // Folded over the renders this origin showed up in, not over every render\n // in the build. A component that also moved somewhere this docket does not\n // group it under moved there for its own reason, and letting that vote\n // would band this change by a page the reviewer is not looking at.\n const cause = rungAcross(group.where.flatMap((each) => each.movement ?? []));\n return {\n component,\n pixels: group.pixels,\n appearances: group.where,\n ...(cause === undefined ? {} : { cause }),\n ...(file === undefined ? {} : { file }),\n ...(reach === null ? {} : { reached: entry !== undefined, ...around(reach, group.where) }),\n ...(entry === undefined ? {} : { trail: entry.trail }),\n };\n })\n .sort((left, right) =>\n right.pixels === left.pixels\n ? left.component.localeCompare(right.component)\n : right.pixels - left.pixels,\n );\n\n return { origins, unattributed };\n}\n\n/**\n * What the diff reaches around this origin, and where it reaches nothing.\n *\n * Read per appearance and then unioned, rather than asked of the component: the\n * question is not *does the commit know this name* but *does the commit arrive in\n * the pictures where this name moved*, and those have different answers exactly\n * when the answer matters.\n *\n * Three piles, not two. A render the reach record has no row for is not a render\n * the commit fails to reach — it is a render nobody asked about — and folding\n * those together is how a report with no per-subject block ends up printing the\n * page's loudest sentence over every change in the build.\n */\nfunction around(\n reach: NonNullable<BuildDetail['reach']>,\n appearances: readonly Appearance[],\n): { through: readonly string[]; stranded: readonly string[]; unlisted: readonly string[] } {\n const through = new Set<string>();\n const stranded: string[] = [];\n const unlisted: string[] = [];\n\n for (const { subject } of appearances) {\n const entry = reach.subjects?.[subject.subject];\n if (entry === undefined) unlisted.push(subject.subject);\n else if (entry.reached) for (const name of entry.through) through.add(name);\n else stranded.push(subject.subject);\n }\n\n return { through: [...through].sort(), stranded, unlisted };\n}\n\n/**\n * The names the run put on this change's cause, for the row to print.\n *\n * The rail used to carry a paragraph per band saying what the band meant — *a\n * component the commit did edit draws each one and hands it what it renders* —\n * while the name of the component that did the drawing sat in the record,\n * unprinted, and the file a component is declared in sat in a `title` attribute\n * nobody hovers. That is backwards twice: the definition is identical on every\n * build, and the name is the only part that is about this one.\n *\n * So the paragraph is gone and this stands where it stood. Per rung, because\n * each rung recorded a different kind of evidence and there is no general one:\n * `edited` has the file the diff named, `token` has the custom properties that\n * took new values, `upstream` has the edited component that reaches this one and\n * the chain it reaches through.\n *\n * Folded over the appearances rather than read off the first, and the difference\n * is the case worth having. One `CardFooter` can be handed its change by\n * `ProductCard` on the product page and by `CartCard` on the cart, and a row\n * printing whichever render came back first would name a page the reviewer is\n * not looking at.\n */\nexport function sourceOf(origin: Origin): readonly string[] {\n const found = new Set<string>();\n\n for (const { movement } of origin.appearances) {\n if (movement === undefined) continue;\n if (movement.cause === 'edited' && movement.file !== undefined) found.add(movement.file);\n if (movement.cause === 'token') for (const token of movement.tokens ?? []) found.add(token);\n if (movement.cause === 'upstream' && movement.upstream !== undefined) {\n found.add([movement.upstream, ...(movement.through ?? [])].join(' → '));\n }\n }\n\n // Where the component is declared, when no movement named anything — which is\n // most of what the `title` attribute was hiding, and is the answer a reviewer\n // opening an unattributed row is about to go looking for anyway.\n if (found.size === 0 && origin.file !== undefined) found.add(origin.file);\n\n return [...found].sort();\n}\n\n/**\n * What the differences look like, which is the half a component name cannot say.\n *\n * A component groups *what was edited*. A shape groups *what the edit did*, and\n * the two answer different questions: eleven appearances under `Button` with one\n * shape between them is a token that moved every button identically, and eleven\n * with nine shapes is a component whose renders each absorbed the change\n * differently. Both are one review. Only the first has a name a reviewer can\n * carry to another build.\n */\nexport function shapesOf(appearances: readonly Appearance[]): Map<string, number> {\n const clusters = new Map<string, number>();\n for (const each of appearances) {\n if (each.shape === undefined) continue;\n clusters.set(each.shape, (clusters.get(each.shape) ?? 0) + 1);\n }\n return clusters;\n}\n"]}
|