@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,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bands a component did not move in on its own, and who moved them.
|
|
3
|
+
*
|
|
4
|
+
* A revision that restyles a button and also adds a label to one of the places
|
|
5
|
+
* that mounts it produces one reading per render, and the change page unions
|
|
6
|
+
* them: *Button moved in what it announces, its layout and its style values*.
|
|
7
|
+
* Two of those three are the button's file. The first is a prop, written in a
|
|
8
|
+
* card, and the button is where it arrived. A reviewer told the button announces
|
|
9
|
+
* something new goes to `button.tsx` and finds nothing that could have done it.
|
|
10
|
+
*
|
|
11
|
+
* Nothing new is measured here. The three records that separate the two already
|
|
12
|
+
* land: which bands moved per render, which subjects each component is drawn in,
|
|
13
|
+
* and which components the diff reaches.
|
|
14
|
+
*
|
|
15
|
+
* ## The control group is the whole claim
|
|
16
|
+
*
|
|
17
|
+
* A band belongs to a parent when the renders it moved in are *exactly* the
|
|
18
|
+
* renders that parent draws — every one of them, and no other. `a11y` moving in
|
|
19
|
+
* the six renders `ProductCard` draws and holding still in the ten it does not
|
|
20
|
+
* is a partition, and a partition is evidence. Overlap is not: `geometry` also
|
|
21
|
+
* moved in the button's own small-size stories, which no card draws, so the
|
|
22
|
+
* cards cannot be what moved it.
|
|
23
|
+
*
|
|
24
|
+
* Which makes the other half load-bearing. **Without renders where the band held
|
|
25
|
+
* still there is no claim to make.** `token` moved everywhere the button is
|
|
26
|
+
* drawn, and every ancestor of the button trivially covers *everywhere* — so a
|
|
27
|
+
* rule that skipped the control group would hand the button's own restyle to
|
|
28
|
+
* whichever card sorted first. `CardFooter` is the honest version of the same
|
|
29
|
+
* shape: it is drawn in six renders, moved in all six, and is very probably
|
|
30
|
+
* carrying somebody else's props — and this says nothing about it, because the
|
|
31
|
+
* suite contains no render that would have distinguished the two.
|
|
32
|
+
*
|
|
33
|
+
* ## Why the holder has to be in the diff
|
|
34
|
+
*
|
|
35
|
+
* A parent the commit never touched hands nothing new down: whatever it passes,
|
|
36
|
+
* it passed in the baseline too. Requiring the reach walk to arrive at the
|
|
37
|
+
* holder is what keeps a coincidence — a container that happens to be drawn in
|
|
38
|
+
* exactly those six renders — from being read as a cause.
|
|
39
|
+
*
|
|
40
|
+
* ## What it will not say
|
|
41
|
+
*
|
|
42
|
+
* That the holder is *why* the pixels differ. It owns the band: the thing that
|
|
43
|
+
* moved in this sense is written in its file, and the decision belongs there.
|
|
44
|
+
* The run's own sentence, on the movement rows, is the answer to *why*.
|
|
45
|
+
*/
|
|
46
|
+
import type { BuildDetail } from '../review-types.js';
|
|
47
|
+
/** One band a component moved in that a parent, not its own file, owns. */
|
|
48
|
+
export interface Handed {
|
|
49
|
+
readonly band: string;
|
|
50
|
+
/** Every parent the partition fits, sorted. Never empty, and rarely not one. */
|
|
51
|
+
readonly holders: readonly string[];
|
|
52
|
+
/** Renders the band moved in — all of them, and all of them drawn by the holders. */
|
|
53
|
+
readonly moved: number;
|
|
54
|
+
/** Renders this component was drawn and read in where the band held still. */
|
|
55
|
+
readonly held: number;
|
|
56
|
+
}
|
|
57
|
+
/** One build's separation of a component's own movement from what it was given. */
|
|
58
|
+
export type Handing = (component: string) => readonly Handed[];
|
|
59
|
+
export declare function handedTo(build: BuildDetail): Handing;
|
|
60
|
+
//# sourceMappingURL=handed.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bands a component did not move in on its own, and who moved them.
|
|
3
|
+
*
|
|
4
|
+
* A revision that restyles a button and also adds a label to one of the places
|
|
5
|
+
* that mounts it produces one reading per render, and the change page unions
|
|
6
|
+
* them: *Button moved in what it announces, its layout and its style values*.
|
|
7
|
+
* Two of those three are the button's file. The first is a prop, written in a
|
|
8
|
+
* card, and the button is where it arrived. A reviewer told the button announces
|
|
9
|
+
* something new goes to `button.tsx` and finds nothing that could have done it.
|
|
10
|
+
*
|
|
11
|
+
* Nothing new is measured here. The three records that separate the two already
|
|
12
|
+
* land: which bands moved per render, which subjects each component is drawn in,
|
|
13
|
+
* and which components the diff reaches.
|
|
14
|
+
*
|
|
15
|
+
* ## The control group is the whole claim
|
|
16
|
+
*
|
|
17
|
+
* A band belongs to a parent when the renders it moved in are *exactly* the
|
|
18
|
+
* renders that parent draws — every one of them, and no other. `a11y` moving in
|
|
19
|
+
* the six renders `ProductCard` draws and holding still in the ten it does not
|
|
20
|
+
* is a partition, and a partition is evidence. Overlap is not: `geometry` also
|
|
21
|
+
* moved in the button's own small-size stories, which no card draws, so the
|
|
22
|
+
* cards cannot be what moved it.
|
|
23
|
+
*
|
|
24
|
+
* Which makes the other half load-bearing. **Without renders where the band held
|
|
25
|
+
* still there is no claim to make.** `token` moved everywhere the button is
|
|
26
|
+
* drawn, and every ancestor of the button trivially covers *everywhere* — so a
|
|
27
|
+
* rule that skipped the control group would hand the button's own restyle to
|
|
28
|
+
* whichever card sorted first. `CardFooter` is the honest version of the same
|
|
29
|
+
* shape: it is drawn in six renders, moved in all six, and is very probably
|
|
30
|
+
* carrying somebody else's props — and this says nothing about it, because the
|
|
31
|
+
* suite contains no render that would have distinguished the two.
|
|
32
|
+
*
|
|
33
|
+
* ## Why the holder has to be in the diff
|
|
34
|
+
*
|
|
35
|
+
* A parent the commit never touched hands nothing new down: whatever it passes,
|
|
36
|
+
* it passed in the baseline too. Requiring the reach walk to arrive at the
|
|
37
|
+
* holder is what keeps a coincidence — a container that happens to be drawn in
|
|
38
|
+
* exactly those six renders — from being read as a cause.
|
|
39
|
+
*
|
|
40
|
+
* ## What it will not say
|
|
41
|
+
*
|
|
42
|
+
* That the holder is *why* the pixels differ. It owns the band: the thing that
|
|
43
|
+
* moved in this sense is written in its file, and the decision belongs there.
|
|
44
|
+
* The run's own sentence, on the movement rows, is the answer to *why*.
|
|
45
|
+
*/
|
|
46
|
+
import { loudestFirst } from './sense.js';
|
|
47
|
+
const NONE = [];
|
|
48
|
+
export function handedTo(build) {
|
|
49
|
+
const census = build.composition;
|
|
50
|
+
// `whole` is the run refusing to attribute the diff at all, which is a missing
|
|
51
|
+
// input and not an empty reach. Either way there is no walk, and with no walk
|
|
52
|
+
// every parent is unreached and nothing here can be said.
|
|
53
|
+
const graph = build.reach !== null && build.reach.whole === undefined ? build.reach : null;
|
|
54
|
+
if (census === null || graph === null)
|
|
55
|
+
return () => NONE;
|
|
56
|
+
const places = new Map(census.map((each) => [each.component, each]));
|
|
57
|
+
const reached = new Set(graph.components.map((each) => each.component));
|
|
58
|
+
// Only the renders that compared hashes. A subject whose baseline carried none
|
|
59
|
+
// is not a render where a band held still — it is a render nobody read, and
|
|
60
|
+
// counting it as a control would let any parent partition anything.
|
|
61
|
+
const read = build.subjects.filter((subject) => subject.moved !== undefined);
|
|
62
|
+
return (component) => {
|
|
63
|
+
const place = places.get(component);
|
|
64
|
+
if (place === undefined)
|
|
65
|
+
return NONE;
|
|
66
|
+
const drawn = new Set(place.subjects);
|
|
67
|
+
const bands = new Map();
|
|
68
|
+
for (const subject of read) {
|
|
69
|
+
if (!drawn.has(subject.subject))
|
|
70
|
+
continue;
|
|
71
|
+
const entry = (subject.moved ?? []).find((each) => each.component === component);
|
|
72
|
+
bands.set(subject.subject, new Set(entry?.bands ?? []));
|
|
73
|
+
}
|
|
74
|
+
if (bands.size === 0)
|
|
75
|
+
return NONE;
|
|
76
|
+
const above = enclosing(component, places).filter((name) => reached.has(name));
|
|
77
|
+
if (above.length === 0)
|
|
78
|
+
return NONE;
|
|
79
|
+
const found = [];
|
|
80
|
+
for (const band of loudestFirst(new Set([...bands.values()].flatMap((each) => [...each])))) {
|
|
81
|
+
const moved = [];
|
|
82
|
+
const held = [];
|
|
83
|
+
for (const [subject, sensed] of bands)
|
|
84
|
+
(sensed.has(band) ? moved : held).push(subject);
|
|
85
|
+
if (held.length === 0)
|
|
86
|
+
continue;
|
|
87
|
+
const holders = above.filter((name) => {
|
|
88
|
+
const subjects = new Set(places.get(name)?.subjects ?? []);
|
|
89
|
+
if (held.some((each) => subjects.has(each)))
|
|
90
|
+
return false;
|
|
91
|
+
return moved.every((each) => subjects.has(each));
|
|
92
|
+
});
|
|
93
|
+
if (holders.length === 0)
|
|
94
|
+
continue;
|
|
95
|
+
found.push({ band, holders: [...holders].sort(), moved: moved.length, held: held.length });
|
|
96
|
+
}
|
|
97
|
+
return found;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Every component that encloses this one anywhere in the suite.
|
|
102
|
+
*
|
|
103
|
+
* All depths, not the nearest: a prop written two rungs up arrives just the
|
|
104
|
+
* same, and the partition is what decides whether the rung is the right one.
|
|
105
|
+
* `seen` starts holding the component itself, so a render tree folded over a
|
|
106
|
+
* suite — a menu inside its own submenu — terminates instead of closing a loop.
|
|
107
|
+
*/
|
|
108
|
+
function enclosing(component, places) {
|
|
109
|
+
const found = new Set();
|
|
110
|
+
const queue = [...(places.get(component)?.within ?? [])];
|
|
111
|
+
const seen = new Set([component]);
|
|
112
|
+
while (queue.length > 0) {
|
|
113
|
+
const name = queue.pop();
|
|
114
|
+
if (name === undefined || seen.has(name))
|
|
115
|
+
continue;
|
|
116
|
+
seen.add(name);
|
|
117
|
+
found.add(name);
|
|
118
|
+
queue.push(...(places.get(name)?.within ?? []));
|
|
119
|
+
}
|
|
120
|
+
return [...found].sort();
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=handed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handed.js","sourceRoot":"","sources":["../../src/ui/handed.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The bands a component did not move in on its own, and who moved them.\n *\n * A revision that restyles a button and also adds a label to one of the places\n * that mounts it produces one reading per render, and the change page unions\n * them: *Button moved in what it announces, its layout and its style values*.\n * Two of those three are the button's file. The first is a prop, written in a\n * card, and the button is where it arrived. A reviewer told the button announces\n * something new goes to `button.tsx` and finds nothing that could have done it.\n *\n * Nothing new is measured here. The three records that separate the two already\n * land: which bands moved per render, which subjects each component is drawn in,\n * and which components the diff reaches.\n *\n * ## The control group is the whole claim\n *\n * A band belongs to a parent when the renders it moved in are *exactly* the\n * renders that parent draws — every one of them, and no other. `a11y` moving in\n * the six renders `ProductCard` draws and holding still in the ten it does not\n * is a partition, and a partition is evidence. Overlap is not: `geometry` also\n * moved in the button's own small-size stories, which no card draws, so the\n * cards cannot be what moved it.\n *\n * Which makes the other half load-bearing. **Without renders where the band held\n * still there is no claim to make.** `token` moved everywhere the button is\n * drawn, and every ancestor of the button trivially covers *everywhere* — so a\n * rule that skipped the control group would hand the button's own restyle to\n * whichever card sorted first. `CardFooter` is the honest version of the same\n * shape: it is drawn in six renders, moved in all six, and is very probably\n * carrying somebody else's props — and this says nothing about it, because the\n * suite contains no render that would have distinguished the two.\n *\n * ## Why the holder has to be in the diff\n *\n * A parent the commit never touched hands nothing new down: whatever it passes,\n * it passed in the baseline too. Requiring the reach walk to arrive at the\n * holder is what keeps a coincidence — a container that happens to be drawn in\n * exactly those six renders — from being read as a cause.\n *\n * ## What it will not say\n *\n * That the holder is *why* the pixels differ. It owns the band: the thing that\n * moved in this sense is written in its file, and the decision belongs there.\n * The run's own sentence, on the movement rows, is the answer to *why*.\n */\n\nimport type { BuildDetail, Placement } from '../review-types.js';\nimport { loudestFirst } from './sense.js';\n\n/** One band a component moved in that a parent, not its own file, owns. */\nexport interface Handed {\n readonly band: string;\n /** Every parent the partition fits, sorted. Never empty, and rarely not one. */\n readonly holders: readonly string[];\n /** Renders the band moved in — all of them, and all of them drawn by the holders. */\n readonly moved: number;\n /** Renders this component was drawn and read in where the band held still. */\n readonly held: number;\n}\n\n/** One build's separation of a component's own movement from what it was given. */\nexport type Handing = (component: string) => readonly Handed[];\n\nconst NONE: readonly Handed[] = [];\n\nexport function handedTo(build: BuildDetail): Handing {\n const census = build.composition;\n // `whole` is the run refusing to attribute the diff at all, which is a missing\n // input and not an empty reach. Either way there is no walk, and with no walk\n // every parent is unreached and nothing here can be said.\n const graph = build.reach !== null && build.reach.whole === undefined ? build.reach : null;\n if (census === null || graph === null) return () => NONE;\n\n const places = new Map(census.map((each) => [each.component, each]));\n const reached = new Set(graph.components.map((each) => each.component));\n\n // Only the renders that compared hashes. A subject whose baseline carried none\n // is not a render where a band held still — it is a render nobody read, and\n // counting it as a control would let any parent partition anything.\n const read = build.subjects.filter((subject) => subject.moved !== undefined);\n\n return (component) => {\n const place = places.get(component);\n if (place === undefined) return NONE;\n\n const drawn = new Set(place.subjects);\n const bands = new Map<string, ReadonlySet<string>>();\n for (const subject of read) {\n if (!drawn.has(subject.subject)) continue;\n const entry = (subject.moved ?? []).find((each) => each.component === component);\n bands.set(subject.subject, new Set<string>(entry?.bands ?? []));\n }\n if (bands.size === 0) return NONE;\n\n const above = enclosing(component, places).filter((name) => reached.has(name));\n if (above.length === 0) return NONE;\n\n const found: Handed[] = [];\n for (const band of loudestFirst(new Set([...bands.values()].flatMap((each) => [...each])))) {\n const moved: string[] = [];\n const held: string[] = [];\n for (const [subject, sensed] of bands) (sensed.has(band) ? moved : held).push(subject);\n if (held.length === 0) continue;\n\n const holders = above.filter((name) => {\n const subjects = new Set(places.get(name)?.subjects ?? []);\n if (held.some((each) => subjects.has(each))) return false;\n return moved.every((each) => subjects.has(each));\n });\n if (holders.length === 0) continue;\n\n found.push({ band, holders: [...holders].sort(), moved: moved.length, held: held.length });\n }\n return found;\n };\n}\n\n/**\n * Every component that encloses this one anywhere in the suite.\n *\n * All depths, not the nearest: a prop written two rungs up arrives just the\n * same, and the partition is what decides whether the rung is the right one.\n * `seen` starts holding the component itself, so a render tree folded over a\n * suite — a menu inside its own submenu — terminates instead of closing a loop.\n */\nfunction enclosing(component: string, places: ReadonlyMap<string, Placement>): readonly string[] {\n const found = new Set<string>();\n const queue = [...(places.get(component)?.within ?? [])];\n const seen = new Set([component]);\n while (queue.length > 0) {\n const name = queue.pop();\n if (name === undefined || seen.has(name)) continue;\n seen.add(name);\n found.add(name);\n queue.push(...(places.get(name)?.within ?? []));\n }\n return [...found].sort();\n}\n"]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import type { Churn, Flakiness, Reach } from '@variance-authority/history';
|
|
3
|
+
import type { SubjectView, TribunalChangelog } from '../review.js';
|
|
4
|
+
import type { ReviewClient } from './client.js';
|
|
5
|
+
/**
|
|
6
|
+
* The two surfaces that read the record rather than the run.
|
|
7
|
+
*
|
|
8
|
+
* A build page answers *what changed here*. Everything in this file answers the
|
|
9
|
+
* question a reviewer actually has to settle before approving: **is this
|
|
10
|
+
* normal?** A 2px shift in a component that has moved in nineteen of the last
|
|
11
|
+
* twenty runs is a different decision from the same 2px in a component nobody has
|
|
12
|
+
* touched since March, and the two are indistinguishable in a before-and-after.
|
|
13
|
+
*
|
|
14
|
+
* ## The changelog is grouped by shape, not listed by approval
|
|
15
|
+
*
|
|
16
|
+
* One token change lands in three hundred subjects, and a list of three hundred
|
|
17
|
+
* approvals is a list nobody reads to the end. So rows are clustered by the
|
|
18
|
+
* fingerprints frozen into them: one entry, the component that owns it, and the
|
|
19
|
+
* subjects it reached. What could not be grouped is *counted and shown* rather
|
|
20
|
+
* than dropped — a changelog covering eleven of forty approvals that looks like a
|
|
21
|
+
* changelog of eleven approvals is worse than no changelog.
|
|
22
|
+
*
|
|
23
|
+
* ## Absent numbers stay absent
|
|
24
|
+
*
|
|
25
|
+
* `Flakiness.rate` is missing when no run has read every subject twice, and it is
|
|
26
|
+
* rendered as missing. Drawing `0%` there answers *how often does this flake*
|
|
27
|
+
* with a confident number derived from a denominator nobody asked for, which is
|
|
28
|
+
* the failure this project is arranged around.
|
|
29
|
+
*/
|
|
30
|
+
export declare function ChangelogPage({ client, onBack, }: {
|
|
31
|
+
readonly client: ReviewClient;
|
|
32
|
+
readonly onBack: () => void;
|
|
33
|
+
}): ReactElement;
|
|
34
|
+
/**
|
|
35
|
+
* The grouped changes, and the approvals no shape could group.
|
|
36
|
+
*
|
|
37
|
+
* The second list is not a footnote. The ephemeral and raster-only paths attribute
|
|
38
|
+
* nothing and still approve something, and a reader who cannot see that a third of
|
|
39
|
+
* the entries are unattributed will read the attributed two thirds as the whole.
|
|
40
|
+
*/
|
|
41
|
+
export declare function ChangelogEntries({ log, filter, }: {
|
|
42
|
+
readonly log: TribunalChangelog;
|
|
43
|
+
/** The component filter in force, so an empty answer can say which one emptied it. */
|
|
44
|
+
readonly filter?: string;
|
|
45
|
+
}): ReactElement;
|
|
46
|
+
/**
|
|
47
|
+
* What the record already knows about the subject in front of the reviewer.
|
|
48
|
+
*
|
|
49
|
+
* Fetched when the panel is opened rather than with the build, and per subject
|
|
50
|
+
* rather than for all of them: a build with three hundred changed subjects would
|
|
51
|
+
* otherwise make nine hundred history requests to draw a page on which a reviewer
|
|
52
|
+
* reads one. The cost of the deferral is a click; the cost of not deferring is the
|
|
53
|
+
* page.
|
|
54
|
+
*
|
|
55
|
+
* The component asked about is the subject's *cause* — the one the semantic tier
|
|
56
|
+
* named — because churn and reach are questions about a component and the region
|
|
57
|
+
* ranked first by area is the container that reflowed.
|
|
58
|
+
*/
|
|
59
|
+
export declare function SubjectHistory({ client, subject, }: {
|
|
60
|
+
readonly client: ReviewClient;
|
|
61
|
+
readonly subject: SubjectView;
|
|
62
|
+
}): ReactElement;
|
|
63
|
+
/**
|
|
64
|
+
* How often this subject has failed to read the same way twice.
|
|
65
|
+
*
|
|
66
|
+
* Two numbers and never one. "Unstable in 6 of 20" and "6 times, none in the last
|
|
67
|
+
* 9 sweeps" are opposite instructions to the person reading them, and only the
|
|
68
|
+
* pair distinguishes a live flake from one somebody already fixed.
|
|
69
|
+
*/
|
|
70
|
+
export declare function StabilityLine({ flakiness }: {
|
|
71
|
+
readonly flakiness: Flakiness;
|
|
72
|
+
}): ReactElement;
|
|
73
|
+
/**
|
|
74
|
+
* How often the cause has changed, and how far it reaches.
|
|
75
|
+
*
|
|
76
|
+
* `changedRuns` counts runs in which this component caused an **approved** change,
|
|
77
|
+
* which is why the approvals route exists: without it every component in the
|
|
78
|
+
* project reads as having never changed. Collateral is reported beside it and never
|
|
79
|
+
* added to it — a component displaced by somebody else's edit is not a component
|
|
80
|
+
* that keeps changing, and summing the two makes the widest container in the
|
|
81
|
+
* application the most volatile thing in it, forever.
|
|
82
|
+
*/
|
|
83
|
+
export declare function ChurnLine({ component, churn, reach, }: {
|
|
84
|
+
readonly component: string;
|
|
85
|
+
readonly churn: Churn;
|
|
86
|
+
readonly reach?: Reach;
|
|
87
|
+
}): ReactElement;
|
|
88
|
+
//# sourceMappingURL=history.d.ts.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { causeOf } from './lead.js';
|
|
4
|
+
import { count, when } from './text.js';
|
|
5
|
+
/**
|
|
6
|
+
* The two surfaces that read the record rather than the run.
|
|
7
|
+
*
|
|
8
|
+
* A build page answers *what changed here*. Everything in this file answers the
|
|
9
|
+
* question a reviewer actually has to settle before approving: **is this
|
|
10
|
+
* normal?** A 2px shift in a component that has moved in nineteen of the last
|
|
11
|
+
* twenty runs is a different decision from the same 2px in a component nobody has
|
|
12
|
+
* touched since March, and the two are indistinguishable in a before-and-after.
|
|
13
|
+
*
|
|
14
|
+
* ## The changelog is grouped by shape, not listed by approval
|
|
15
|
+
*
|
|
16
|
+
* One token change lands in three hundred subjects, and a list of three hundred
|
|
17
|
+
* approvals is a list nobody reads to the end. So rows are clustered by the
|
|
18
|
+
* fingerprints frozen into them: one entry, the component that owns it, and the
|
|
19
|
+
* subjects it reached. What could not be grouped is *counted and shown* rather
|
|
20
|
+
* than dropped — a changelog covering eleven of forty approvals that looks like a
|
|
21
|
+
* changelog of eleven approvals is worse than no changelog.
|
|
22
|
+
*
|
|
23
|
+
* ## Absent numbers stay absent
|
|
24
|
+
*
|
|
25
|
+
* `Flakiness.rate` is missing when no run has read every subject twice, and it is
|
|
26
|
+
* rendered as missing. Drawing `0%` there answers *how often does this flake*
|
|
27
|
+
* with a confident number derived from a denominator nobody asked for, which is
|
|
28
|
+
* the failure this project is arranged around.
|
|
29
|
+
*/
|
|
30
|
+
export function ChangelogPage({ client, onBack, }) {
|
|
31
|
+
const [component, setComponent] = useState('');
|
|
32
|
+
const [applied, setApplied] = useState('');
|
|
33
|
+
const [log, setLog] = useState({ state: 'loading' });
|
|
34
|
+
const load = useCallback(async () => {
|
|
35
|
+
setLog({ state: 'loading' });
|
|
36
|
+
try {
|
|
37
|
+
setLog({
|
|
38
|
+
state: 'ready',
|
|
39
|
+
value: await client.changelog(applied === '' ? {} : { component: applied }),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
setLog({ state: 'failed', why: messageOf(error) });
|
|
44
|
+
}
|
|
45
|
+
}, [client, applied]);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
void load();
|
|
48
|
+
}, [load]);
|
|
49
|
+
return (_jsxs(_Fragment, { children: [_jsxs("header", { className: "va-topbar", children: [_jsx("button", { type: "button", className: "va-back", onClick: onBack, children: "\u2190" }), _jsxs("span", { className: "va-topbar-title", children: [_jsx("strong", { children: "Changelog" }), _jsx("span", { className: "va-topbar-sub", children: "every approval on record" })] })] }), _jsx("div", { className: "va-body va-scroll", children: _jsxs("article", { className: "va-page va-changelog", children: [_jsx("p", { className: "va-subtitle", children: "Why the baselines are what they are: every approval, grouped by the shape that was approved." }), _jsxs("form", { className: "va-filter", onSubmit: (event) => {
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
setApplied(component.trim());
|
|
52
|
+
}, children: [_jsx("label", { htmlFor: "va-changelog-component", children: "component" }), _jsx("input", { id: "va-changelog-component", value: component, placeholder: "any component", onChange: (event) => setComponent(event.target.value) }), _jsx("button", { type: "submit", children: "Filter" }), applied === '' ? null : (_jsx("button", { type: "button", onClick: () => {
|
|
53
|
+
setComponent('');
|
|
54
|
+
setApplied('');
|
|
55
|
+
}, children: "Clear" }))] }), log.state === 'loading' ? _jsx("p", { className: "va-note", children: "Loading the changelog\u2026" }) : null, log.state === 'failed' ? _jsx(Failure, { why: log.why, retry: load }) : null, log.state === 'ready' ? (_jsx(ChangelogEntries, { log: log.value, ...(applied === '' ? {} : { filter: applied }) })) : null] }) })] }));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The grouped changes, and the approvals no shape could group.
|
|
59
|
+
*
|
|
60
|
+
* The second list is not a footnote. The ephemeral and raster-only paths attribute
|
|
61
|
+
* nothing and still approve something, and a reader who cannot see that a third of
|
|
62
|
+
* the entries are unattributed will read the attributed two thirds as the whole.
|
|
63
|
+
*/
|
|
64
|
+
export function ChangelogEntries({ log, filter, }) {
|
|
65
|
+
if (log.changes.length === 0 && log.ungrouped.length === 0) {
|
|
66
|
+
// Two sentences, because a filtered empty answer and an empty project are
|
|
67
|
+
// different facts and only one of them is about the project. Told the first
|
|
68
|
+
// as the second, a reviewer concludes nothing has ever been approved here.
|
|
69
|
+
return (_jsx("p", { className: "va-note", children: filter === undefined
|
|
70
|
+
? 'Nothing has been approved in this project yet.'
|
|
71
|
+
: `No approval matched “${filter}”. The project may still have approvals under other components.` }));
|
|
72
|
+
}
|
|
73
|
+
return (_jsxs(_Fragment, { children: [_jsx("ol", { className: "va-changes", children: log.changes.map((change) => (_jsx(ChangeEntry, { change: change }, change.fingerprint))) }), log.ungrouped.length === 0 ? null : (_jsxs("section", { className: "va-ungrouped", children: [_jsxs("h2", { children: [log.ungrouped.length, " approved without an attributed shape"] }), _jsx("p", { className: "va-note", children: "These runs kept no region attribution \u2014 an ephemeral or raster-only path \u2014 so nothing names what changed in them. Listed rather than dropped: they are approvals, and a total that omits them is not a total." }), _jsx("ul", { children: log.ungrouped.map((row) => (_jsxs("li", { children: [_jsx("strong", { children: row.subject }), " \u2014 ", row.by, " \u00B7 ", _jsx("code", { children: row.commit.slice(0, 8) }), " \u00B7", ' ', when(row.at)] }, `${row.build}/${row.subject}`))) })] }))] }));
|
|
74
|
+
}
|
|
75
|
+
function ChangeEntry({ change }) {
|
|
76
|
+
return (_jsxs("li", { className: "va-change", children: [_jsxs("h3", { children: [change.component ?? 'an unnamed component', change.file === undefined ? null : (_jsxs(_Fragment, { children: [' ', _jsx("code", { children: change.file })] }))] }), _jsxs("p", { className: "va-change-meta", children: [change.subjects.length, " subject", change.subjects.length === 1 ? '' : 's', " \u00B7", ' ', change.builds.length, " build", change.builds.length === 1 ? '' : 's', " \u00B7 approved by", ' ', change.by.join(', '), " \u00B7 ", when(change.at)] }), change.intent === undefined ? null : (_jsxs("p", { className: "va-intent", children: ["Declared intent: ", _jsx("em", { children: change.intent })] })), change.note === undefined ? null : _jsx("p", { className: "va-change-note", children: change.note }), _jsxs("details", { children: [_jsx("summary", { children: "subjects" }), _jsx("ul", { className: "va-change-subjects", children: change.subjects.map((subject) => (_jsx("li", { children: subject }, subject))) })] }), _jsxs("p", { className: "va-fingerprint", children: ["shape ", _jsx("code", { children: change.fingerprint.slice(0, 12) }), " \u2014 the identity the grouping is computed from, so a shape approved across three sessions reads as one change."] })] }));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* What the record already knows about the subject in front of the reviewer.
|
|
80
|
+
*
|
|
81
|
+
* Fetched when the panel is opened rather than with the build, and per subject
|
|
82
|
+
* rather than for all of them: a build with three hundred changed subjects would
|
|
83
|
+
* otherwise make nine hundred history requests to draw a page on which a reviewer
|
|
84
|
+
* reads one. The cost of the deferral is a click; the cost of not deferring is the
|
|
85
|
+
* page.
|
|
86
|
+
*
|
|
87
|
+
* The component asked about is the subject's *cause* — the one the semantic tier
|
|
88
|
+
* named — because churn and reach are questions about a component and the region
|
|
89
|
+
* ranked first by area is the container that reflowed.
|
|
90
|
+
*/
|
|
91
|
+
export function SubjectHistory({ client, subject, }) {
|
|
92
|
+
const component = causeOf(subject);
|
|
93
|
+
const [asked, setAsked] = useState(false);
|
|
94
|
+
const [answer, setAnswer] = useState({ state: 'loading' });
|
|
95
|
+
const load = useCallback(async () => {
|
|
96
|
+
setAnswer({ state: 'loading' });
|
|
97
|
+
try {
|
|
98
|
+
const [flakiness, churn, reach] = await Promise.all([
|
|
99
|
+
client.flakiness(subject.subject),
|
|
100
|
+
component === undefined ? undefined : client.churn(component),
|
|
101
|
+
component === undefined ? undefined : client.reach(component),
|
|
102
|
+
]);
|
|
103
|
+
setAnswer({
|
|
104
|
+
state: 'ready',
|
|
105
|
+
flakiness,
|
|
106
|
+
...(churn === undefined ? {} : { churn }),
|
|
107
|
+
...(reach === undefined ? {} : { reach }),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
setAnswer({ state: 'failed', why: messageOf(error) });
|
|
112
|
+
}
|
|
113
|
+
}, [client, subject.subject, component]);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (asked)
|
|
116
|
+
void load();
|
|
117
|
+
}, [asked, load]);
|
|
118
|
+
if (!asked) {
|
|
119
|
+
return (_jsx("p", { className: "va-history", children: _jsx("button", { type: "button", className: "va-ask", onClick: () => setAsked(true), children: "Has this changed before?" }) }));
|
|
120
|
+
}
|
|
121
|
+
if (answer.state === 'loading')
|
|
122
|
+
return _jsx("p", { className: "va-note", children: "Reading the record\u2026" });
|
|
123
|
+
if (answer.state === 'failed')
|
|
124
|
+
return _jsx(Failure, { why: answer.why, retry: load });
|
|
125
|
+
return (_jsxs("section", { className: "va-history", children: [_jsx(StabilityLine, { flakiness: answer.flakiness }), answer.churn === undefined || component === undefined ? (_jsx("p", { className: "va-note", children: "No component was named as the cause of this difference, so there is nothing to ask the record about. The regions came from a run that attributed nothing." })) : (_jsx(ChurnLine, { component: component, churn: answer.churn, ...(answer.reach === undefined ? {} : { reach: answer.reach }) }))] }));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* How often this subject has failed to read the same way twice.
|
|
129
|
+
*
|
|
130
|
+
* Two numbers and never one. "Unstable in 6 of 20" and "6 times, none in the last
|
|
131
|
+
* 9 sweeps" are opposite instructions to the person reading them, and only the
|
|
132
|
+
* pair distinguishes a live flake from one somebody already fixed.
|
|
133
|
+
*/
|
|
134
|
+
export function StabilityLine({ flakiness }) {
|
|
135
|
+
if (flakiness.sweeps === 0) {
|
|
136
|
+
return (_jsx("p", { className: "va-stability va-unknown", children: "Nothing has read this subject twice in the recorded window, so how stable it is is unknown \u2014 not stable. A rate needs a run that swept." }));
|
|
137
|
+
}
|
|
138
|
+
if (flakiness.occurrences === 0) {
|
|
139
|
+
return (_jsxs("p", { className: "va-stability", children: ["Read twice in ", flakiness.sweeps, " run", flakiness.sweeps === 1 ? '' : 's', " and never disagreed with itself."] }));
|
|
140
|
+
}
|
|
141
|
+
return (_jsxs("p", { className: "va-stability va-flaky", children: ["Read differently in ", flakiness.occurrences, " of ", flakiness.sweeps, " sweeps", flakiness.rate === undefined ? null : _jsxs(_Fragment, { children: [" (", Math.round(flakiness.rate * 100), "%)"] }), flakiness.sweepsSince === 0
|
|
142
|
+
? ', most recently in the latest sweep'
|
|
143
|
+
: `, none in the last ${flakiness.sweepsSince} sweep${flakiness.sweepsSince === 1 ? '' : 's'}`, ".", flakiness.causes.length === 0 ? null : (_jsxs(_Fragment, { children: [' ', "Loudest cause: ", _jsx("strong", { children: flakiness.causes[0]?.component ?? 'unnamed' }), flakiness.causes[0]?.band === undefined ? null : _jsxs(_Fragment, { children: [" (", flakiness.causes[0].band, ")"] }), "."] })), flakiness.absorbedRuns === 0 ? null : (_jsxs(_Fragment, { children: [' ', count(flakiness.absorbedRuns, 'further run'), " moved only in bands this subject declared it does not assert on."] }))] }));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* How often the cause has changed, and how far it reaches.
|
|
147
|
+
*
|
|
148
|
+
* `changedRuns` counts runs in which this component caused an **approved** change,
|
|
149
|
+
* which is why the approvals route exists: without it every component in the
|
|
150
|
+
* project reads as having never changed. Collateral is reported beside it and never
|
|
151
|
+
* added to it — a component displaced by somebody else's edit is not a component
|
|
152
|
+
* that keeps changing, and summing the two makes the widest container in the
|
|
153
|
+
* application the most volatile thing in it, forever.
|
|
154
|
+
*/
|
|
155
|
+
export function ChurnLine({ component, churn, reach, }) {
|
|
156
|
+
if (churn.runs === 0) {
|
|
157
|
+
// `0 of 0` reads as a component that has never moved, which is the opposite
|
|
158
|
+
// of what an empty window says. The same distinction `StabilityLine` draws:
|
|
159
|
+
// a rate nobody has a run for is unknown, not zero.
|
|
160
|
+
return (_jsxs("p", { className: "va-churn va-unknown", children: ["No recorded run carries ", _jsx("strong", { children: component }), ", so how often it changes is unknown \u2014 not none."] }));
|
|
161
|
+
}
|
|
162
|
+
return (_jsxs("p", { className: "va-churn", children: [_jsx("strong", { children: component }), " caused an approved change in ", churn.changedRuns, " of ", churn.runs, ' ', "recorded run", churn.runs === 1 ? '' : 's', churn.lastAt === undefined ? null : _jsxs(_Fragment, { children: [", most recently ", churn.lastAt] }), ".", churn.collateralRuns === 0 ? null : (_jsxs(_Fragment, { children: [" It was displaced without changing in ", churn.collateralRuns, " more."] })), churn.rejectedRuns === 0 ? null : _jsxs(_Fragment, { children: [" ", churn.rejectedRuns, " change to it was rejected."] }), reach === undefined ? null : (_jsxs(_Fragment, { children: [' ', "It is observed in ", reach.subjects.length, " subject", reach.subjects.length === 1 ? '' : 's', reach.arrived.length === 0 ? null : _jsxs(_Fragment, { children: [", having arrived in ", reach.arrived.length, " of them in this window"] }), "."] }))] }));
|
|
163
|
+
}
|
|
164
|
+
function Failure({ why, retry }) {
|
|
165
|
+
return (_jsxs("p", { className: "va-failure", children: [why, ' ', _jsx("button", { type: "button", onClick: retry, children: "retry" })] }));
|
|
166
|
+
}
|
|
167
|
+
function messageOf(error) {
|
|
168
|
+
return error instanceof Error ? error.message : String(error);
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.js","sourceRoot":"","sources":["../../src/ui/history.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAqB,MAAM,OAAO,CAAC;AAI5E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAExC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,UAAU,aAAa,CAAC,EAC5B,MAAM,EACN,MAAM,GAIP;IACC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC/C,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC3C,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,QAAQ,CAK5B,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAExB,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,IAAmB,EAAE;QACjD,MAAM,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,CAAC;gBACL,KAAK,EAAE,OAAO;gBACd,KAAK,EAAE,MAAM,MAAM,CAAC,SAAS,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;aAC5E,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACrD,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAEtB,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,IAAI,EAAE,CAAC;IACd,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,OAAO,CACL,8BACE,kBAAQ,SAAS,EAAC,WAAW,aAC3B,iBAAQ,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,SAAS,EAAC,OAAO,EAAE,MAAM,uBAEhD,EACT,gBAAM,SAAS,EAAC,iBAAiB,aAC/B,yCAA0B,EAC1B,eAAM,SAAS,EAAC,eAAe,yCAAgC,IAC1D,IACA,EAET,cAAK,SAAS,EAAC,mBAAmB,YAChC,mBAAS,SAAS,EAAC,sBAAsB,aAC3C,YAAG,SAAS,EAAC,aAAa,6GAEtB,EAEJ,gBACE,SAAS,EAAC,WAAW,EACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;gCAClB,KAAK,CAAC,cAAc,EAAE,CAAC;gCACvB,UAAU,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;4BAC/B,CAAC,aAED,gBAAO,OAAO,EAAC,wBAAwB,0BAAkB,EACzD,gBACE,EAAE,EAAC,wBAAwB,EAC3B,KAAK,EAAE,SAAS,EAChB,WAAW,EAAC,eAAe,EAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,GACrD,EACF,iBAAQ,IAAI,EAAC,QAAQ,uBAAgB,EACpC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACvB,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE;wCACZ,YAAY,CAAC,EAAE,CAAC,CAAC;wCACjB,UAAU,CAAC,EAAE,CAAC,CAAC;oCACjB,CAAC,sBAGM,CACV,IACI,EAEN,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,YAAG,SAAS,EAAC,SAAS,4CAA2B,CAAC,CAAC,CAAC,IAAI,EAClF,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAC,OAAO,IAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,GAAI,CAAC,CAAC,CAAC,IAAI,EACtE,GAAG,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,CACvB,KAAC,gBAAgB,IAAC,GAAG,EAAE,GAAG,CAAC,KAAK,KAAM,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAI,CACtF,CAAC,CAAC,CAAC,IAAI,IACI,GACN,IACL,CACJ,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAC/B,GAAG,EACH,MAAM,GAKP;IACC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3D,0EAA0E;QAC1E,4EAA4E;QAC5E,2EAA2E;QAC3E,OAAO,CACL,YAAG,SAAS,EAAC,SAAS,YACnB,MAAM,KAAK,SAAS;gBACnB,CAAC,CAAC,gDAAgD;gBAClD,CAAC,CAAC,wBAAwB,MAAM,iEAAiE,GACjG,CACL,CAAC;IACJ,CAAC;IAED,OAAO,CACL,8BACE,aAAI,SAAS,EAAC,YAAY,YACvB,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAC3B,KAAC,WAAW,IAA0B,MAAM,EAAE,MAAM,IAAlC,MAAM,CAAC,WAAW,CAAoB,CACzD,CAAC,GACC,EAEJ,GAAG,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACnC,mBAAS,SAAS,EAAC,cAAc,aAC/B,yBAAK,GAAG,CAAC,SAAS,CAAC,MAAM,6CAA2C,EACpE,YAAG,SAAS,EAAC,SAAS,wOAIlB,EACJ,uBACG,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAiB,EAAE,EAAE,CAAC,CACxC,yBACE,2BAAS,GAAG,CAAC,OAAO,GAAU,cAAI,GAAG,CAAC,EAAE,cAAI,yBAAO,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAQ,aAAG,GAAG,EACtF,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,KAFN,GAAG,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,CAGjC,CACN,CAAC,GACC,IACG,CACX,IACA,CACJ,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,MAAM,EAAwC;IACnE,OAAO,CACL,cAAI,SAAS,EAAC,WAAW,aACvB,yBACG,MAAM,CAAC,SAAS,IAAI,sBAAsB,EAC1C,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAClC,8BACG,GAAG,EACJ,yBAAO,MAAM,CAAC,IAAI,GAAQ,IACzB,CACJ,IACE,EACL,aAAG,SAAS,EAAC,gBAAgB,aAC1B,MAAM,CAAC,QAAQ,CAAC,MAAM,cAAU,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,aAAI,GAAG,EAC9E,MAAM,CAAC,MAAM,CAAC,MAAM,YAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,yBAAgB,GAAG,EACpF,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,cAAK,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,IACvC,EACH,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACpC,aAAG,SAAS,EAAC,WAAW,kCACL,uBAAK,MAAM,CAAC,MAAM,GAAM,IACvC,CACL,EACA,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,YAAG,SAAS,EAAC,gBAAgB,YAAE,MAAM,CAAC,IAAI,GAAK,EACnF,8BACE,yCAA2B,EAC3B,aAAI,SAAS,EAAC,oBAAoB,YAC/B,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAChC,uBAAmB,OAAO,IAAjB,OAAO,CAAgB,CACjC,CAAC,GACC,IACG,EACV,aAAG,SAAS,EAAC,gBAAgB,uBACrB,yBAAO,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAQ,0HAElD,IACD,CACN,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,EAC7B,MAAM,EACN,OAAO,GAIR;IACC,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC1C,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CASlC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAExB,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,IAAmB,EAAE;QACjD,SAAS,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QAChC,IAAI,CAAC;YACH,MAAM,CAAC,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBAClD,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC;gBACjC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC;gBAC7D,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC;aAC9D,CAAC,CAAC;YACH,SAAS,CAAC;gBACR,KAAK,EAAE,OAAO;gBACd,SAAS;gBACT,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;gBACzC,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;aAC1C,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,SAAS,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACxD,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;IAEzC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,KAAK;YAAE,KAAK,IAAI,EAAE,CAAC;IACzB,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IAElB,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CACL,YAAG,SAAS,EAAC,YAAY,YACvB,iBAAQ,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,QAAQ,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,yCAE7D,GACP,CACL,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,YAAG,SAAS,EAAC,SAAS,yCAAwB,CAAC;IACtF,IAAI,MAAM,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAC,OAAO,IAAC,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,GAAI,CAAC;IAEhF,OAAO,CACL,mBAAS,SAAS,EAAC,YAAY,aAC7B,KAAC,aAAa,IAAC,SAAS,EAAE,MAAM,CAAC,SAAS,GAAI,EAC7C,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,CACvD,YAAG,SAAS,EAAC,SAAS,0KAGlB,CACL,CAAC,CAAC,CAAC,CACF,KAAC,SAAS,IACR,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,MAAM,CAAC,KAAK,KACf,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,GAC/D,CACH,IACO,CACX,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,SAAS,EAAqC;IAC5E,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,CACL,YAAG,SAAS,EAAC,yBAAyB,6JAGlC,CACL,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,CAAC,WAAW,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,CACL,aAAG,SAAS,EAAC,cAAc,+BACV,SAAS,CAAC,MAAM,UAAM,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,yCAEpE,CACL,CAAC;IACJ,CAAC;IAED,OAAO,CACL,aAAG,SAAS,EAAC,uBAAuB,qCACb,SAAS,CAAC,WAAW,UAAM,SAAS,CAAC,MAAM,aAC/D,SAAS,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,oCAAK,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,GAAG,GAAG,CAAC,UAAM,EACjF,SAAS,CAAC,WAAW,KAAK,CAAC;gBAC1B,CAAC,CAAC,qCAAqC;gBACvC,CAAC,CAAC,sBAAsB,SAAS,CAAC,WAAW,SAAS,SAAS,CAAC,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,OAE/F,SAAS,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACtC,8BACG,GAAG,qBACW,2BAAS,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,IAAI,SAAS,GAAU,EAC5E,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,oCAAK,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,SAAK,SACnF,CACJ,EACA,SAAS,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACrC,8BACG,GAAG,EACH,KAAK,CAAC,SAAS,CAAC,YAAY,EAAE,aAAa,CAAC,yEAE5C,CACJ,IACC,CACL,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS,CAAC,EACxB,SAAS,EACT,KAAK,EACL,KAAK,GAKN;IACC,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACrB,4EAA4E;QAC5E,4EAA4E;QAC5E,oDAAoD;QACpD,OAAO,CACL,aAAG,SAAS,EAAC,qBAAqB,yCACR,2BAAS,SAAS,GAAU,6DAElD,CACL,CAAC;IACJ,CAAC;IAED,OAAO,CACL,aAAG,SAAS,EAAC,UAAU,aACrB,2BAAS,SAAS,GAAU,oCAA+B,KAAK,CAAC,WAAW,UAAM,KAAK,CAAC,IAAI,EAAE,GAAG,kBACpF,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EACvC,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,kDAAmB,KAAK,CAAC,MAAM,IAAI,OACvE,KAAK,CAAC,cAAc,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACnC,wEAAyC,KAAK,CAAC,cAAc,cAAU,CACxE,EACA,KAAK,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,mCAAI,KAAK,CAAC,YAAY,mCAA+B,EACvF,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC5B,8BACG,GAAG,wBACe,KAAK,CAAC,QAAQ,CAAC,MAAM,cACvC,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EACtC,KAAK,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,sDAAuB,KAAK,CAAC,OAAO,CAAC,MAAM,+BAA2B,SAE1G,CACJ,IACC,CACL,CAAC;AACJ,CAAC;AAGD,SAAS,OAAO,CAAC,EAAE,GAAG,EAAE,KAAK,EAAwD;IACnF,OAAO,CACL,aAAG,SAAS,EAAC,YAAY,aACtB,GAAG,EAAE,GAAG,EACT,iBAAQ,IAAI,EAAC,QAAQ,EAAC,OAAO,EAAE,KAAK,sBAE3B,IACP,CACL,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC","sourcesContent":["import { useCallback, useEffect, useState, type ReactElement } from 'react';\nimport type { Churn, Flakiness, Reach } from '@variance-authority/history';\nimport type { ChangelogChange, ChangelogRow, SubjectView, TribunalChangelog } from '../review.js';\nimport type { ReviewClient } from './client.js';\nimport { causeOf } from './lead.js';\nimport { count, when } from './text.js';\n\n/**\n * The two surfaces that read the record rather than the run.\n *\n * A build page answers *what changed here*. Everything in this file answers the\n * question a reviewer actually has to settle before approving: **is this\n * normal?** A 2px shift in a component that has moved in nineteen of the last\n * twenty runs is a different decision from the same 2px in a component nobody has\n * touched since March, and the two are indistinguishable in a before-and-after.\n *\n * ## The changelog is grouped by shape, not listed by approval\n *\n * One token change lands in three hundred subjects, and a list of three hundred\n * approvals is a list nobody reads to the end. So rows are clustered by the\n * fingerprints frozen into them: one entry, the component that owns it, and the\n * subjects it reached. What could not be grouped is *counted and shown* rather\n * than dropped — a changelog covering eleven of forty approvals that looks like a\n * changelog of eleven approvals is worse than no changelog.\n *\n * ## Absent numbers stay absent\n *\n * `Flakiness.rate` is missing when no run has read every subject twice, and it is\n * rendered as missing. Drawing `0%` there answers *how often does this flake*\n * with a confident number derived from a denominator nobody asked for, which is\n * the failure this project is arranged around.\n */\n\nexport function ChangelogPage({\n client,\n onBack,\n}: {\n readonly client: ReviewClient;\n readonly onBack: () => void;\n}): ReactElement {\n const [component, setComponent] = useState('');\n const [applied, setApplied] = useState('');\n const [log, setLog] = useState<\n { readonly state: 'loading' } | { readonly state: 'failed'; readonly why: string } | {\n readonly state: 'ready';\n readonly value: TribunalChangelog;\n }\n >({ state: 'loading' });\n\n const load = useCallback(async (): Promise<void> => {\n setLog({ state: 'loading' });\n try {\n setLog({\n state: 'ready',\n value: await client.changelog(applied === '' ? {} : { component: applied }),\n });\n } catch (error) {\n setLog({ state: 'failed', why: messageOf(error) });\n }\n }, [client, applied]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n return (\n <>\n <header className=\"va-topbar\">\n <button type=\"button\" className=\"va-back\" onClick={onBack}>\n ←\n </button>\n <span className=\"va-topbar-title\">\n <strong>Changelog</strong>\n <span className=\"va-topbar-sub\">every approval on record</span>\n </span>\n </header>\n\n <div className=\"va-body va-scroll\">\n <article className=\"va-page va-changelog\">\n <p className=\"va-subtitle\">\n Why the baselines are what they are: every approval, grouped by the shape that was approved.\n </p>\n\n <form\n className=\"va-filter\"\n onSubmit={(event) => {\n event.preventDefault();\n setApplied(component.trim());\n }}\n >\n <label htmlFor=\"va-changelog-component\">component</label>\n <input\n id=\"va-changelog-component\"\n value={component}\n placeholder=\"any component\"\n onChange={(event) => setComponent(event.target.value)}\n />\n <button type=\"submit\">Filter</button>\n {applied === '' ? null : (\n <button\n type=\"button\"\n onClick={() => {\n setComponent('');\n setApplied('');\n }}\n >\n Clear\n </button>\n )}\n </form>\n\n {log.state === 'loading' ? <p className=\"va-note\">Loading the changelog…</p> : null}\n {log.state === 'failed' ? <Failure why={log.why} retry={load} /> : null}\n {log.state === 'ready' ? (\n <ChangelogEntries log={log.value} {...(applied === '' ? {} : { filter: applied })} />\n ) : null}\n </article>\n </div>\n </>\n );\n}\n\n/**\n * The grouped changes, and the approvals no shape could group.\n *\n * The second list is not a footnote. The ephemeral and raster-only paths attribute\n * nothing and still approve something, and a reader who cannot see that a third of\n * the entries are unattributed will read the attributed two thirds as the whole.\n */\nexport function ChangelogEntries({\n log,\n filter,\n}: {\n readonly log: TribunalChangelog;\n /** The component filter in force, so an empty answer can say which one emptied it. */\n readonly filter?: string;\n}): ReactElement {\n if (log.changes.length === 0 && log.ungrouped.length === 0) {\n // Two sentences, because a filtered empty answer and an empty project are\n // different facts and only one of them is about the project. Told the first\n // as the second, a reviewer concludes nothing has ever been approved here.\n return (\n <p className=\"va-note\">\n {filter === undefined\n ? 'Nothing has been approved in this project yet.'\n : `No approval matched “${filter}”. The project may still have approvals under other components.`}\n </p>\n );\n }\n\n return (\n <>\n <ol className=\"va-changes\">\n {log.changes.map((change) => (\n <ChangeEntry key={change.fingerprint} change={change} />\n ))}\n </ol>\n\n {log.ungrouped.length === 0 ? null : (\n <section className=\"va-ungrouped\">\n <h2>{log.ungrouped.length} approved without an attributed shape</h2>\n <p className=\"va-note\">\n These runs kept no region attribution — an ephemeral or raster-only path — so nothing\n names what changed in them. Listed rather than dropped: they are approvals, and a total\n that omits them is not a total.\n </p>\n <ul>\n {log.ungrouped.map((row: ChangelogRow) => (\n <li key={`${row.build}/${row.subject}`}>\n <strong>{row.subject}</strong> — {row.by} · <code>{row.commit.slice(0, 8)}</code> ·{' '}\n {when(row.at)}\n </li>\n ))}\n </ul>\n </section>\n )}\n </>\n );\n}\n\nfunction ChangeEntry({ change }: { readonly change: ChangelogChange }): ReactElement {\n return (\n <li className=\"va-change\">\n <h3>\n {change.component ?? 'an unnamed component'}\n {change.file === undefined ? null : (\n <>\n {' '}\n <code>{change.file}</code>\n </>\n )}\n </h3>\n <p className=\"va-change-meta\">\n {change.subjects.length} subject{change.subjects.length === 1 ? '' : 's'} ·{' '}\n {change.builds.length} build{change.builds.length === 1 ? '' : 's'} · approved by{' '}\n {change.by.join(', ')} · {when(change.at)}\n </p>\n {change.intent === undefined ? null : (\n <p className=\"va-intent\">\n Declared intent: <em>{change.intent}</em>\n </p>\n )}\n {change.note === undefined ? null : <p className=\"va-change-note\">{change.note}</p>}\n <details>\n <summary>subjects</summary>\n <ul className=\"va-change-subjects\">\n {change.subjects.map((subject) => (\n <li key={subject}>{subject}</li>\n ))}\n </ul>\n </details>\n <p className=\"va-fingerprint\">\n shape <code>{change.fingerprint.slice(0, 12)}</code> — the identity the grouping is computed\n from, so a shape approved across three sessions reads as one change.\n </p>\n </li>\n );\n}\n\n/**\n * What the record already knows about the subject in front of the reviewer.\n *\n * Fetched when the panel is opened rather than with the build, and per subject\n * rather than for all of them: a build with three hundred changed subjects would\n * otherwise make nine hundred history requests to draw a page on which a reviewer\n * reads one. The cost of the deferral is a click; the cost of not deferring is the\n * page.\n *\n * The component asked about is the subject's *cause* — the one the semantic tier\n * named — because churn and reach are questions about a component and the region\n * ranked first by area is the container that reflowed.\n */\nexport function SubjectHistory({\n client,\n subject,\n}: {\n readonly client: ReviewClient;\n readonly subject: SubjectView;\n}): ReactElement {\n const component = causeOf(subject);\n const [asked, setAsked] = useState(false);\n const [answer, setAnswer] = useState<\n | { readonly state: 'loading' }\n | { readonly state: 'failed'; readonly why: string }\n | {\n readonly state: 'ready';\n readonly flakiness: Flakiness;\n readonly churn?: Churn;\n readonly reach?: Reach;\n }\n >({ state: 'loading' });\n\n const load = useCallback(async (): Promise<void> => {\n setAnswer({ state: 'loading' });\n try {\n const [flakiness, churn, reach] = await Promise.all([\n client.flakiness(subject.subject),\n component === undefined ? undefined : client.churn(component),\n component === undefined ? undefined : client.reach(component),\n ]);\n setAnswer({\n state: 'ready',\n flakiness,\n ...(churn === undefined ? {} : { churn }),\n ...(reach === undefined ? {} : { reach }),\n });\n } catch (error) {\n setAnswer({ state: 'failed', why: messageOf(error) });\n }\n }, [client, subject.subject, component]);\n\n useEffect(() => {\n if (asked) void load();\n }, [asked, load]);\n\n if (!asked) {\n return (\n <p className=\"va-history\">\n <button type=\"button\" className=\"va-ask\" onClick={() => setAsked(true)}>\n Has this changed before?\n </button>\n </p>\n );\n }\n\n if (answer.state === 'loading') return <p className=\"va-note\">Reading the record…</p>;\n if (answer.state === 'failed') return <Failure why={answer.why} retry={load} />;\n\n return (\n <section className=\"va-history\">\n <StabilityLine flakiness={answer.flakiness} />\n {answer.churn === undefined || component === undefined ? (\n <p className=\"va-note\">\n No component was named as the cause of this difference, so there is nothing to ask the\n record about. The regions came from a run that attributed nothing.\n </p>\n ) : (\n <ChurnLine\n component={component}\n churn={answer.churn}\n {...(answer.reach === undefined ? {} : { reach: answer.reach })}\n />\n )}\n </section>\n );\n}\n\n/**\n * How often this subject has failed to read the same way twice.\n *\n * Two numbers and never one. \"Unstable in 6 of 20\" and \"6 times, none in the last\n * 9 sweeps\" are opposite instructions to the person reading them, and only the\n * pair distinguishes a live flake from one somebody already fixed.\n */\nexport function StabilityLine({ flakiness }: { readonly flakiness: Flakiness }): ReactElement {\n if (flakiness.sweeps === 0) {\n return (\n <p className=\"va-stability va-unknown\">\n Nothing has read this subject twice in the recorded window, so how stable it is is unknown —\n not stable. A rate needs a run that swept.\n </p>\n );\n }\n\n if (flakiness.occurrences === 0) {\n return (\n <p className=\"va-stability\">\n Read twice in {flakiness.sweeps} run{flakiness.sweeps === 1 ? '' : 's'} and never disagreed\n with itself.\n </p>\n );\n }\n\n return (\n <p className=\"va-stability va-flaky\">\n Read differently in {flakiness.occurrences} of {flakiness.sweeps} sweeps\n {flakiness.rate === undefined ? null : <> ({Math.round(flakiness.rate * 100)}%)</>}\n {flakiness.sweepsSince === 0\n ? ', most recently in the latest sweep'\n : `, none in the last ${flakiness.sweepsSince} sweep${flakiness.sweepsSince === 1 ? '' : 's'}`}\n .\n {flakiness.causes.length === 0 ? null : (\n <>\n {' '}\n Loudest cause: <strong>{flakiness.causes[0]?.component ?? 'unnamed'}</strong>\n {flakiness.causes[0]?.band === undefined ? null : <> ({flakiness.causes[0].band})</>}.\n </>\n )}\n {flakiness.absorbedRuns === 0 ? null : (\n <>\n {' '}\n {count(flakiness.absorbedRuns, 'further run')} moved only in bands this subject declared it\n does not assert on.\n </>\n )}\n </p>\n );\n}\n\n/**\n * How often the cause has changed, and how far it reaches.\n *\n * `changedRuns` counts runs in which this component caused an **approved** change,\n * which is why the approvals route exists: without it every component in the\n * project reads as having never changed. Collateral is reported beside it and never\n * added to it — a component displaced by somebody else's edit is not a component\n * that keeps changing, and summing the two makes the widest container in the\n * application the most volatile thing in it, forever.\n */\nexport function ChurnLine({\n component,\n churn,\n reach,\n}: {\n readonly component: string;\n readonly churn: Churn;\n readonly reach?: Reach;\n}): ReactElement {\n if (churn.runs === 0) {\n // `0 of 0` reads as a component that has never moved, which is the opposite\n // of what an empty window says. The same distinction `StabilityLine` draws:\n // a rate nobody has a run for is unknown, not zero.\n return (\n <p className=\"va-churn va-unknown\">\n No recorded run carries <strong>{component}</strong>, so how often it changes is unknown —\n not none.\n </p>\n );\n }\n\n return (\n <p className=\"va-churn\">\n <strong>{component}</strong> caused an approved change in {churn.changedRuns} of {churn.runs}{' '}\n recorded run{churn.runs === 1 ? '' : 's'}\n {churn.lastAt === undefined ? null : <>, most recently {churn.lastAt}</>}.\n {churn.collateralRuns === 0 ? null : (\n <> It was displaced without changing in {churn.collateralRuns} more.</>\n )}\n {churn.rejectedRuns === 0 ? null : <> {churn.rejectedRuns} change to it was rejected.</>}\n {reach === undefined ? null : (\n <>\n {' '}\n It is observed in {reach.subjects.length} subject\n {reach.subjects.length === 1 ? '' : 's'}\n {reach.arrived.length === 0 ? null : <>, having arrived in {reach.arrived.length} of them in this window</>}\n .\n </>\n )}\n </p>\n );\n}\n\n\nfunction Failure({ why, retry }: { readonly why: string; readonly retry: () => void }): ReactElement {\n return (\n <p className=\"va-failure\">\n {why}{' '}\n <button type=\"button\" onClick={retry}>\n retry\n </button>\n </p>\n );\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What draws a component, when nothing the commit touched declares it.
|
|
3
|
+
*
|
|
4
|
+
* The build page could count these and not explain them. *Two components moved
|
|
5
|
+
* that no file in this commit declares* is arithmetic over the reach walk, and
|
|
6
|
+
* the reach walk goes one way: from a file the diff named, up through the files
|
|
7
|
+
* that import it, to the components those files declare. Everything a changed
|
|
8
|
+
* file *draws* is off that graph by construction — `ProductCard` renders `Card`,
|
|
9
|
+
* `Card` renders `CardFooter`, and no number of importers arrives at
|
|
10
|
+
* `CardFooter`. So the page had a true sentence with a shrug where its reason
|
|
11
|
+
* should be.
|
|
12
|
+
*
|
|
13
|
+
* The run already wrote the other direction down. Every component in the census
|
|
14
|
+
* carries the components that enclose it, folded over the whole suite, and a
|
|
15
|
+
* walk up that from `CardFooter` reaches `ProductCard` in two steps — which is
|
|
16
|
+
* the file the commit changed.
|
|
17
|
+
*
|
|
18
|
+
* ## Two constraints, and both of them are what makes the answer true
|
|
19
|
+
*
|
|
20
|
+
* **The census is suite-wide.** `Card.within` is every component that encloses a
|
|
21
|
+
* `Card` anywhere, so an unconstrained walk out of `CardFooter` arrives at
|
|
22
|
+
* `CartCard` as readily as at `ProductCard` — and `CartCard` is the cart, which
|
|
23
|
+
* has no `CardFooter` in it. Every step is therefore held to the subjects the
|
|
24
|
+
* component actually moved in.
|
|
25
|
+
*
|
|
26
|
+
* **The walk stops at the first rung the commit reaches.** Not the first
|
|
27
|
+
* enclosure: `Card` encloses `CardFooter` and explains nothing, because nothing
|
|
28
|
+
* in the diff arrives at `Card` either. The answer a reviewer can act on is the
|
|
29
|
+
* nearest enclosure that is *also* a component the commit reaches, and the
|
|
30
|
+
* components in between are the path they will read.
|
|
31
|
+
*
|
|
32
|
+
* ## What it will not say
|
|
33
|
+
*
|
|
34
|
+
* That the enclosure is the cause. It is an enclosure — the commit reaches a
|
|
35
|
+
* component that draws this one, in a subject where this one moved — and that is
|
|
36
|
+
* the strongest statement the two records support together. A component can be
|
|
37
|
+
* drawn by an edited parent and have moved for an unrelated reason.
|
|
38
|
+
*/
|
|
39
|
+
import type { BuildDetail } from '../review-types.js';
|
|
40
|
+
/** A component the commit reaches, and the chain from it down to the one asked about. */
|
|
41
|
+
export interface Enclosure {
|
|
42
|
+
readonly holder: string;
|
|
43
|
+
/** Components between the two, outermost first. Empty when the holder draws it. */
|
|
44
|
+
readonly through: readonly string[];
|
|
45
|
+
}
|
|
46
|
+
export type Holding =
|
|
47
|
+
/** The run kept no census, so nothing here was measured. */
|
|
48
|
+
{
|
|
49
|
+
readonly kind: 'unrecorded';
|
|
50
|
+
}
|
|
51
|
+
/** The census has no row for this name — it was never a boundary anywhere. */
|
|
52
|
+
| {
|
|
53
|
+
readonly kind: 'unlisted';
|
|
54
|
+
}
|
|
55
|
+
/** Nothing in the suite encloses it: wherever it appears, it is the outermost thing. */
|
|
56
|
+
| {
|
|
57
|
+
readonly kind: 'outermost';
|
|
58
|
+
}
|
|
59
|
+
/** Enclosed, and the commit reaches nothing that encloses it at any depth. */
|
|
60
|
+
| {
|
|
61
|
+
readonly kind: 'enclosed';
|
|
62
|
+
readonly within: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Enclosed, and this build read no diff — so *reaches* has no value here.
|
|
66
|
+
*
|
|
67
|
+
* Apart from `enclosed`, which is a finding. With no graph to walk, every
|
|
68
|
+
* component in the suite would return one, and a page saying *the commit
|
|
69
|
+
* reaches none of them* on all of them would have turned a missing input into
|
|
70
|
+
* a screen of alarms.
|
|
71
|
+
*/
|
|
72
|
+
| {
|
|
73
|
+
readonly kind: 'unmeasured';
|
|
74
|
+
readonly within: readonly string[];
|
|
75
|
+
}
|
|
76
|
+
/** Every nearest enclosure the commit reaches, sorted by holder. Never empty. */
|
|
77
|
+
| {
|
|
78
|
+
readonly kind: 'through';
|
|
79
|
+
readonly by: readonly Enclosure[];
|
|
80
|
+
};
|
|
81
|
+
/** Who draws one component, over one build's census. */
|
|
82
|
+
export type Enclosing = (component: string, subjects: readonly string[]) => Holding;
|
|
83
|
+
export declare function heldBy(build: BuildDetail): Enclosing;
|
|
84
|
+
//# sourceMappingURL=holding.d.ts.map
|