@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,90 @@
|
|
|
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
|
+
export function heldBy(build) {
|
|
40
|
+
const census = build.composition;
|
|
41
|
+
if (census === null)
|
|
42
|
+
return () => ({ kind: 'unrecorded' });
|
|
43
|
+
const places = new Map(census.map((each) => [each.component, each]));
|
|
44
|
+
const graph = build.reach !== null && build.reach.whole === undefined ? build.reach : null;
|
|
45
|
+
const reached = new Set(graph?.components.map((each) => each.component) ?? []);
|
|
46
|
+
return (component, subjects) => {
|
|
47
|
+
const start = places.get(component);
|
|
48
|
+
if (start === undefined)
|
|
49
|
+
return { kind: 'unlisted' };
|
|
50
|
+
if (start.within.length === 0)
|
|
51
|
+
return { kind: 'outermost' };
|
|
52
|
+
// An empty subject list is *nobody said where it moved*, and a walk that
|
|
53
|
+
// read it as a filter would exclude every rung and report a component with
|
|
54
|
+
// an obvious parent as enclosed by nothing the commit reaches.
|
|
55
|
+
const where = new Set(subjects);
|
|
56
|
+
const shares = (name) => {
|
|
57
|
+
const place = places.get(name);
|
|
58
|
+
if (place === undefined)
|
|
59
|
+
return false;
|
|
60
|
+
return where.size === 0 || place.subjects.some((subject) => where.has(subject));
|
|
61
|
+
};
|
|
62
|
+
// Breadth-first, and `seen` is written as a rung is *built* rather than as
|
|
63
|
+
// it is walked: a component enclosed by two things on the same rung would
|
|
64
|
+
// otherwise be queued twice and reported twice under one name.
|
|
65
|
+
const seen = new Set([component]);
|
|
66
|
+
const step = (from, within) => {
|
|
67
|
+
const rung = [];
|
|
68
|
+
for (const holder of within) {
|
|
69
|
+
if (seen.has(holder) || !shares(holder))
|
|
70
|
+
continue;
|
|
71
|
+
seen.add(holder);
|
|
72
|
+
rung.push({ holder, through: from === null ? [] : [from.holder, ...from.through] });
|
|
73
|
+
}
|
|
74
|
+
return rung;
|
|
75
|
+
};
|
|
76
|
+
let rung = step(null, start.within);
|
|
77
|
+
while (rung.length > 0) {
|
|
78
|
+
const arrived = rung.filter((each) => reached.has(each.holder));
|
|
79
|
+
if (arrived.length > 0) {
|
|
80
|
+
return {
|
|
81
|
+
kind: 'through',
|
|
82
|
+
by: [...arrived].sort((left, right) => left.holder.localeCompare(right.holder)),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
rung = rung.flatMap((each) => step(each, places.get(each.holder)?.within ?? []));
|
|
86
|
+
}
|
|
87
|
+
return { kind: graph === null ? 'unmeasured' : 'enclosed', within: start.within };
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=holding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"holding.js","sourceRoot":"","sources":["../../src/ui/holding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAmCH,MAAM,UAAU,MAAM,CAAC,KAAkB;IACvC,MAAM,MAAM,GAAG,KAAK,CAAC,WAAW,CAAC;IACjC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;IAE3D,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IACrE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3F,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;IAE/E,OAAO,CAAC,SAAS,EAAE,QAAQ,EAAW,EAAE;QACtC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACpC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;QACrD,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;QAE5D,yEAAyE;QACzE,2EAA2E;QAC3E,+DAA+D;QAC/D,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;QAChC,MAAM,MAAM,GAAG,CAAC,IAAY,EAAW,EAAE;YACvC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC/B,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,KAAK,CAAC;YACtC,OAAO,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QAClF,CAAC,CAAC;QAEF,2EAA2E;QAC3E,0EAA0E;QAC1E,+DAA+D;QAC/D,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,CAAC,IAAsB,EAAE,MAAyB,EAAe,EAAE;YAC9E,MAAM,IAAI,GAAgB,EAAE,CAAC;YAC7B,KAAK,MAAM,MAAM,IAAI,MAAM,EAAE,CAAC;gBAC5B,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;oBAAE,SAAS;gBAClD,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACjB,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACtF,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;QAEF,IAAI,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;YAChE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACvB,OAAO;oBACL,IAAI,EAAE,SAAS;oBACf,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;iBAChF,CAAC;YACJ,CAAC;YACD,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC;QACnF,CAAC;QAED,OAAO,EAAE,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;IACpF,CAAC,CAAC;AACJ,CAAC","sourcesContent":["/**\n * What draws a component, when nothing the commit touched declares it.\n *\n * The build page could count these and not explain them. *Two components moved\n * that no file in this commit declares* is arithmetic over the reach walk, and\n * the reach walk goes one way: from a file the diff named, up through the files\n * that import it, to the components those files declare. Everything a changed\n * file *draws* is off that graph by construction — `ProductCard` renders `Card`,\n * `Card` renders `CardFooter`, and no number of importers arrives at\n * `CardFooter`. So the page had a true sentence with a shrug where its reason\n * should be.\n *\n * The run already wrote the other direction down. Every component in the census\n * carries the components that enclose it, folded over the whole suite, and a\n * walk up that from `CardFooter` reaches `ProductCard` in two steps — which is\n * the file the commit changed.\n *\n * ## Two constraints, and both of them are what makes the answer true\n *\n * **The census is suite-wide.** `Card.within` is every component that encloses a\n * `Card` anywhere, so an unconstrained walk out of `CardFooter` arrives at\n * `CartCard` as readily as at `ProductCard` — and `CartCard` is the cart, which\n * has no `CardFooter` in it. Every step is therefore held to the subjects the\n * component actually moved in.\n *\n * **The walk stops at the first rung the commit reaches.** Not the first\n * enclosure: `Card` encloses `CardFooter` and explains nothing, because nothing\n * in the diff arrives at `Card` either. The answer a reviewer can act on is the\n * nearest enclosure that is *also* a component the commit reaches, and the\n * components in between are the path they will read.\n *\n * ## What it will not say\n *\n * That the enclosure is the cause. It is an enclosure — the commit reaches a\n * component that draws this one, in a subject where this one moved — and that is\n * the strongest statement the two records support together. A component can be\n * drawn by an edited parent and have moved for an unrelated reason.\n */\n\nimport type { BuildDetail } from '../review-types.js';\n\n/** A component the commit reaches, and the chain from it down to the one asked about. */\nexport interface Enclosure {\n readonly holder: string;\n /** Components between the two, outermost first. Empty when the holder draws it. */\n readonly through: readonly string[];\n}\n\nexport type Holding =\n /** The run kept no census, so nothing here was measured. */\n | { readonly kind: 'unrecorded' }\n /** The census has no row for this name — it was never a boundary anywhere. */\n | { readonly kind: 'unlisted' }\n /** Nothing in the suite encloses it: wherever it appears, it is the outermost thing. */\n | { readonly kind: 'outermost' }\n /** Enclosed, and the commit reaches nothing that encloses it at any depth. */\n | { readonly kind: 'enclosed'; readonly within: readonly string[] }\n /**\n * Enclosed, and this build read no diff — so *reaches* has no value here.\n *\n * Apart from `enclosed`, which is a finding. With no graph to walk, every\n * component in the suite would return one, and a page saying *the commit\n * reaches none of them* on all of them would have turned a missing input into\n * a screen of alarms.\n */\n | { readonly kind: 'unmeasured'; readonly within: readonly string[] }\n /** Every nearest enclosure the commit reaches, sorted by holder. Never empty. */\n | { readonly kind: 'through'; readonly by: readonly Enclosure[] };\n\n/** Who draws one component, over one build's census. */\nexport type Enclosing = (component: string, subjects: readonly string[]) => Holding;\n\nexport function heldBy(build: BuildDetail): Enclosing {\n const census = build.composition;\n if (census === null) return () => ({ kind: 'unrecorded' });\n\n const places = new Map(census.map((each) => [each.component, each]));\n const graph = build.reach !== null && build.reach.whole === undefined ? build.reach : null;\n const reached = new Set(graph?.components.map((each) => each.component) ?? []);\n\n return (component, subjects): Holding => {\n const start = places.get(component);\n if (start === undefined) return { kind: 'unlisted' };\n if (start.within.length === 0) return { kind: 'outermost' };\n\n // An empty subject list is *nobody said where it moved*, and a walk that\n // read it as a filter would exclude every rung and report a component with\n // an obvious parent as enclosed by nothing the commit reaches.\n const where = new Set(subjects);\n const shares = (name: string): boolean => {\n const place = places.get(name);\n if (place === undefined) return false;\n return where.size === 0 || place.subjects.some((subject) => where.has(subject));\n };\n\n // Breadth-first, and `seen` is written as a rung is *built* rather than as\n // it is walked: a component enclosed by two things on the same rung would\n // otherwise be queued twice and reported twice under one name.\n const seen = new Set([component]);\n const step = (from: Enclosure | null, within: readonly string[]): Enclosure[] => {\n const rung: Enclosure[] = [];\n for (const holder of within) {\n if (seen.has(holder) || !shares(holder)) continue;\n seen.add(holder);\n rung.push({ holder, through: from === null ? [] : [from.holder, ...from.through] });\n }\n return rung;\n };\n\n let rung = step(null, start.within);\n while (rung.length > 0) {\n const arrived = rung.filter((each) => reached.has(each.holder));\n if (arrived.length > 0) {\n return {\n kind: 'through',\n by: [...arrived].sort((left, right) => left.holder.localeCompare(right.holder)),\n };\n }\n rung = rung.flatMap((each) => step(each, places.get(each.holder)?.within ?? []));\n }\n\n return { kind: graph === null ? 'unmeasured' : 'enclosed', within: start.within };\n };\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this build is a build *of*, and how far the edit landed from itself.
|
|
3
|
+
*
|
|
4
|
+
* A docket names components. It never named the commit those components are a
|
|
5
|
+
* consequence of, and a reviewer arriving at a build had the one thing they could
|
|
6
|
+
* not get from the pixels — the diff — sitting in the store, unprinted. Three
|
|
7
|
+
* facts, in the order somebody reads them:
|
|
8
|
+
*
|
|
9
|
+
* - **The commits.** A build is one commit, and a build following another build
|
|
10
|
+
* is a range. The range is the accumulation: two builds apart, the changes on
|
|
11
|
+
* this page are the consequence of everything between them, and a single hash
|
|
12
|
+
* at the top of the screen invites the reader to blame the last one.
|
|
13
|
+
* - **The files.** `reach.changed` is the diff, already read, already stored. It
|
|
14
|
+
* is the shortest answer to *why did anything move at all*, and until now it
|
|
15
|
+
* was reachable only from a second page.
|
|
16
|
+
* - **How far it went.** Depth from an edited file to a component that moved.
|
|
17
|
+
*
|
|
18
|
+
* The third is the one worth the space. A commit whose movement is all at depth
|
|
19
|
+
* zero did what it said; a commit with eleven components moving three imports out
|
|
20
|
+
* changed something shared, and that is the difference between an afternoon and a
|
|
21
|
+
* revert. It is measurable here and nowhere else on the surface — every other
|
|
22
|
+
* page is scoped to one change, and depth is a property of the whole spread.
|
|
23
|
+
*/
|
|
24
|
+
import type { ReactElement } from 'react';
|
|
25
|
+
import type { BuildDetail } from '../review-types.js';
|
|
26
|
+
import type { Crossing } from './crossing.js';
|
|
27
|
+
export declare function Impact({ build, crossing, }: {
|
|
28
|
+
readonly build: BuildDetail;
|
|
29
|
+
readonly crossing: Crossing;
|
|
30
|
+
}): ReactElement;
|
|
31
|
+
//# sourceMappingURL=impact.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { attributionsOf } from './attribution.js';
|
|
3
|
+
import { Marked } from './because.js';
|
|
4
|
+
import { spreadOf } from './distance.js';
|
|
5
|
+
import { heldBy } from './holding.js';
|
|
6
|
+
import { count, number } from './text.js';
|
|
7
|
+
/** How many paths are printed before the rest become a count. */
|
|
8
|
+
const NAMED = 10;
|
|
9
|
+
export function Impact({ build, crossing, }) {
|
|
10
|
+
const earlier = crossing.state === 'ready' ? crossing.earlier : undefined;
|
|
11
|
+
const since = earlier?.commit === build.commit ? undefined : earlier;
|
|
12
|
+
return (_jsxs("section", { className: "va-impact", children: [_jsxs("p", { className: "va-impact-commit", children: [since === undefined ? null : (_jsxs(_Fragment, { children: [_jsx("code", { className: "va-commit", children: since.commit.slice(0, 8) }), _jsx("span", { className: "va-note", children: " \u2026 " })] })), _jsx("code", { className: "va-commit", children: build.commit.slice(0, 8) }), build.branch === undefined ? null : _jsxs("span", { className: "va-note", children: [" on ", build.branch] }), since === undefined ? null : (_jsxs("span", { className: "va-note", children: [' ', "\u2014 everything since build ", since.build, ", not one commit"] }))] }), _jsx(Changed, { build: build }), _jsx(Spread, { build: build })] }));
|
|
13
|
+
}
|
|
14
|
+
/** The diff itself: what the commit touched, and against what. */
|
|
15
|
+
function Changed({ build }) {
|
|
16
|
+
const { reach } = build;
|
|
17
|
+
if (reach === null) {
|
|
18
|
+
return (_jsx("p", { className: "va-note", children: "No diff was read for this build, so nothing here can say which files it came from." }));
|
|
19
|
+
}
|
|
20
|
+
if (reach.whole !== undefined) {
|
|
21
|
+
return (_jsxs("p", { className: "va-note", children: ["The diff against ", _jsx("code", { children: reach.against }), " could not be attributed: ", reach.whole] }));
|
|
22
|
+
}
|
|
23
|
+
const named = reach.changed.slice(0, NAMED);
|
|
24
|
+
const rest = reach.changed.length - named.length;
|
|
25
|
+
return (_jsxs("div", { className: "va-impact-files", children: [_jsxs("p", { className: "va-note", children: [count(reach.changed.length, 'file'), " changed against ", _jsx("code", { children: reach.against })] }), _jsxs("ul", { title: reach.changed.join('\n'), children: [named.map((file) => (_jsx("li", { children: _jsx("code", { children: file }) }, file))), rest === 0 ? null : _jsxs("li", { className: "va-note", children: ["and ", count(rest, 'more file')] })] })] }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The impact-depth histogram, and the one sentence derived from it.
|
|
29
|
+
*
|
|
30
|
+
* The sentence is the deepest rung anything moved on, because that is the number
|
|
31
|
+
* a reviewer would otherwise have to read off the bars themselves — and a chart
|
|
32
|
+
* whose headline the reader has to compute is a chart that gets skipped.
|
|
33
|
+
*/
|
|
34
|
+
function Spread({ build }) {
|
|
35
|
+
const spread = spreadOf(build);
|
|
36
|
+
if (spread === null || spread.rungs.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
const deepest = [...spread.rungs].reverse().find((rung) => rung.moved > 0);
|
|
39
|
+
const widest = Math.max(...spread.rungs.map((rung) => rung.reached));
|
|
40
|
+
return (_jsxs("div", { className: "va-impact-depth", children: [_jsx("h2", { children: "How far the edit landed" }), _jsx("p", { className: "va-note", children: deepest === undefined
|
|
41
|
+
? 'Nothing the commit declares moved.'
|
|
42
|
+
: deepest.depth === 0
|
|
43
|
+
? 'Movement in what the commit declares stops at the files it changed.'
|
|
44
|
+
: `Movement reaches ${count(deepest.depth, 'import')} out from an edited file.` }), _jsx("ul", { className: "va-rungs", children: spread.rungs.map((rung) => (_jsx(Rung, { rung: rung, widest: widest }, rung.depth))) }), _jsx(Undeclared, { build: build, moved: spread.undeclared }), spread.throughUnread === 0 ? null : (_jsxs("p", { className: "va-note", children: [count(spread.throughUnread, 'component'), " reached only through a file the scan could not read, so their depth is measured from the blind spot rather than from the edit \u2014 they are not counted above."] }))] }));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The movement the histogram cannot hold, and the reason for each of it.
|
|
48
|
+
*
|
|
49
|
+
* Depth is measured from an edited file, so a component no changed file declares
|
|
50
|
+
* has nothing to be measured from and drops out of every bar above. This said *no
|
|
51
|
+
* rung above holds them* and stopped there, which restates the axis instead of
|
|
52
|
+
* answering the question — and it was answerable twice over, because the run had
|
|
53
|
+
* already climbed the other direction and written a sentence per movement.
|
|
54
|
+
*
|
|
55
|
+
* So the run's own sentence is what this prints. The census walk in
|
|
56
|
+
* [`holding.ts`](./holding.js) is kept underneath it and used only where the
|
|
57
|
+
* store holds no attribution — a build ingested before they were carried, or a
|
|
58
|
+
* run that composed no census — and every state it can return still gets its own
|
|
59
|
+
* clause, because a component nothing draws and a component nobody measured are
|
|
60
|
+
* different facts.
|
|
61
|
+
*/
|
|
62
|
+
function Undeclared({ build, moved, }) {
|
|
63
|
+
if (moved.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
const held = heldBy(build);
|
|
66
|
+
const attributed = attributionsOf(build);
|
|
67
|
+
return (_jsxs("div", { className: "va-unheld", children: [_jsxs("p", { children: [count(moved.length, 'component'), " no file in this commit declares \u2014 not in the bars above:"] }), _jsx("ul", { children: moved.map((each) => {
|
|
68
|
+
const recorded = attributed.about(each.component)[0];
|
|
69
|
+
return (_jsxs("li", { title: each.subjects.join('\n'), children: [_jsx("span", { className: "va-unheld-name", children: each.component }), _jsx("span", { className: "va-unheld-why", children: recorded === undefined ? (drawnBy(held(each.component, each.subjects))) : (_jsx(Marked, { say: recorded.because })) })] }, each.component));
|
|
70
|
+
}) })] }));
|
|
71
|
+
}
|
|
72
|
+
/** Who draws it, as the clause that follows its name. */
|
|
73
|
+
function drawnBy(holding) {
|
|
74
|
+
switch (holding.kind) {
|
|
75
|
+
case 'unrecorded':
|
|
76
|
+
return 'this run recorded no composition, so nothing here knows what draws it';
|
|
77
|
+
case 'unlisted':
|
|
78
|
+
return 'the run drew no boundary under this name, so nothing here knows what draws it';
|
|
79
|
+
case 'outermost':
|
|
80
|
+
return 'nothing in the suite draws it — wherever it appears, it is the outermost component';
|
|
81
|
+
case 'enclosed':
|
|
82
|
+
return `drawn by ${listing(holding.within)}, and the commit reaches none of them`;
|
|
83
|
+
case 'unmeasured':
|
|
84
|
+
return `drawn by ${listing(holding.within)}, and no diff was read to place them against`;
|
|
85
|
+
case 'through':
|
|
86
|
+
return chains(holding.by);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The chains, one clause each, grouped by the path they take.
|
|
91
|
+
*
|
|
92
|
+
* Two holders that draw a component the same way are one sentence — *MainNav and
|
|
93
|
+
* NavItem draw it* — because the reviewer is being told a route, and printing
|
|
94
|
+
* the route twice under two names is the same route twice.
|
|
95
|
+
*/
|
|
96
|
+
function chains(by) {
|
|
97
|
+
const routes = new Map();
|
|
98
|
+
for (const each of by) {
|
|
99
|
+
const route = routes.get(each.through.join(' > ')) ?? { through: each.through, holders: [] };
|
|
100
|
+
route.holders.push(each.holder);
|
|
101
|
+
routes.set(each.through.join(' > '), route);
|
|
102
|
+
}
|
|
103
|
+
return [...routes.values()]
|
|
104
|
+
.map(({ through, holders }) => {
|
|
105
|
+
const draws = holders.length === 1 ? 'draws' : 'draw';
|
|
106
|
+
const [first, ...rest] = [...through, 'it'];
|
|
107
|
+
const tail = rest.map((name) => `which draws ${name}`).join(', ');
|
|
108
|
+
return `${listing(holders)} ${draws} ${first ?? 'it'}${tail === '' ? '' : `, ${tail}`}`;
|
|
109
|
+
})
|
|
110
|
+
.join('; ');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Names as a reader would say them, and a count once there are too many to say.
|
|
114
|
+
*
|
|
115
|
+
* The cap is on the enclosure list, which is suite-wide: a `Card` inside forty
|
|
116
|
+
* things would otherwise put forty names in a sentence whose point is that none
|
|
117
|
+
* of them is the one the reviewer wants.
|
|
118
|
+
*/
|
|
119
|
+
function listing(names, cap = 3) {
|
|
120
|
+
if (names.length > cap) {
|
|
121
|
+
return `${names.slice(0, cap).join(', ')} and ${count(names.length - cap, 'other')}`;
|
|
122
|
+
}
|
|
123
|
+
if (names.length <= 1)
|
|
124
|
+
return names.join('');
|
|
125
|
+
return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1] ?? ''}`;
|
|
126
|
+
}
|
|
127
|
+
/** One depth: everything the commit reaches there, and how much of it moved. */
|
|
128
|
+
function Rung({ rung, widest }) {
|
|
129
|
+
const label = rung.depth === 0 ? 'in a changed file' : `${count(rung.depth, 'import')} out`;
|
|
130
|
+
return (_jsxs("li", { className: "va-rung", children: [_jsx("span", { className: "va-rung-at va-note", children: label }), _jsx("span", { className: "va-rung-bar", style: { width: `${String((rung.reached / widest) * 100)}%` }, children: _jsx("span", { className: "va-rung-moved", style: { width: `${String((rung.moved / rung.reached) * 100)}%` } }) }), _jsxs("span", { className: "va-rung-num va-num", children: [number(rung.moved), "/", number(rung.reached)] })] }));
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=impact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"impact.js","sourceRoot":"","sources":["../../src/ui/impact.tsx"],"names":[],"mappings":";AA0BA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,OAAO,EAAE,QAAQ,EAA6B,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,MAAM,EAAgC,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAE1C,iEAAiE;AACjE,MAAM,KAAK,GAAG,EAAE,CAAC;AAEjB,MAAM,UAAU,MAAM,CAAC,EACrB,KAAK,EACL,QAAQ,GAIT;IACC,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1E,MAAM,KAAK,GAAG,OAAO,EAAE,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;IAErE,OAAO,CACL,mBAAS,SAAS,EAAC,WAAW,aAC5B,aAAG,SAAS,EAAC,kBAAkB,aAC5B,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC5B,8BACE,eAAM,SAAS,EAAC,WAAW,YAAE,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAQ,EAC7D,eAAM,SAAS,EAAC,SAAS,yBAAW,IACnC,CACJ,EACD,eAAM,SAAS,EAAC,WAAW,YAAE,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAQ,EAC5D,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,gBAAM,SAAS,EAAC,SAAS,qBAAM,KAAK,CAAC,MAAM,IAAQ,EACvF,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC5B,gBAAM,SAAS,EAAC,SAAS,aACtB,GAAG,oCACsB,KAAK,CAAC,KAAK,wBAChC,CACR,IACC,EAEJ,KAAC,OAAO,IAAC,KAAK,EAAE,KAAK,GAAI,EACzB,KAAC,MAAM,IAAC,KAAK,EAAE,KAAK,GAAI,IAChB,CACX,CAAC;AACJ,CAAC;AAED,kEAAkE;AAClE,SAAS,OAAO,CAAC,EAAE,KAAK,EAAmC;IACzD,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IAExB,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,CACL,YAAG,SAAS,EAAC,SAAS,mGAElB,CACL,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,CACL,aAAG,SAAS,EAAC,SAAS,kCACH,yBAAO,KAAK,CAAC,OAAO,GAAQ,gCAA2B,KAAK,CAAC,KAAK,IACjF,CACL,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAEjD,OAAO,CACL,eAAK,SAAS,EAAC,iBAAiB,aAC9B,aAAG,SAAS,EAAC,SAAS,aACnB,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,uBAAkB,yBAAO,KAAK,CAAC,OAAO,GAAQ,IAChF,EACJ,cAAI,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,aAChC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACnB,uBACE,yBAAO,IAAI,GAAQ,IADZ,IAAI,CAER,CACN,CAAC,EACD,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,cAAI,SAAS,EAAC,SAAS,qBAAM,KAAK,CAAC,IAAI,EAAE,WAAW,CAAC,IAAM,IAC7E,IACD,CACP,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAS,MAAM,CAAC,EAAE,KAAK,EAAmC;IACxD,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE9D,MAAM,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAC3E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAErE,OAAO,CACL,eAAK,SAAS,EAAC,iBAAiB,aAC9B,mDAAgC,EAChC,YAAG,SAAS,EAAC,SAAS,YACnB,OAAO,KAAK,SAAS;oBACpB,CAAC,CAAC,oCAAoC;oBACtC,CAAC,CAAC,OAAO,CAAC,KAAK,KAAK,CAAC;wBACnB,CAAC,CAAC,qEAAqE;wBACvE,CAAC,CAAC,oBAAoB,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,2BAA2B,GACjF,EACJ,aAAI,SAAS,EAAC,UAAU,YACrB,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAC1B,KAAC,IAAI,IAAkB,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,IAAtC,IAAI,CAAC,KAAK,CAAgC,CACtD,CAAC,GACC,EACL,KAAC,UAAU,IAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,UAAU,GAAI,EACrD,MAAM,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACnC,aAAG,SAAS,EAAC,SAAS,aACnB,KAAK,CAAC,MAAM,CAAC,aAAa,EAAE,WAAW,CAAC,yKAGvC,CACL,IACG,CACP,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAS,UAAU,CAAC,EAClB,KAAK,EACL,KAAK,GAIN;IACC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3B,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAEzC,OAAO,CACL,eAAK,SAAS,EAAC,WAAW,aACxB,wBAIG,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,sEAC/B,EACJ,uBACG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;oBAClB,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;oBACrD,OAAO,CACL,cAAyB,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,aACtD,eAAM,SAAS,EAAC,gBAAgB,YAAE,IAAI,CAAC,SAAS,GAAQ,EACxD,eAAM,SAAS,EAAC,eAAe,YAC5B,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,CACxB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAC7C,CAAC,CAAC,CAAC,CACF,KAAC,MAAM,IAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,GAAI,CAClC,GACI,KARA,IAAI,CAAC,SAAS,CASlB,CACN,CAAC;gBACJ,CAAC,CAAC,GACC,IACD,CACP,CAAC;AACJ,CAAC;AAED,yDAAyD;AACzD,SAAS,OAAO,CAAC,OAAgB;IAC/B,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;QACrB,KAAK,YAAY;YACf,OAAO,uEAAuE,CAAC;QACjF,KAAK,UAAU;YACb,OAAO,+EAA+E,CAAC;QACzF,KAAK,WAAW;YACd,OAAO,oFAAoF,CAAC;QAC9F,KAAK,UAAU;YACb,OAAO,YAAY,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,uCAAuC,CAAC;QACpF,KAAK,YAAY;YACf,OAAO,YAAY,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,8CAA8C,CAAC;QAC3F,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,MAAM,CAAC,EAAwB;IACtC,MAAM,MAAM,GAAG,IAAI,GAAG,EAA6D,CAAC;IACpF,KAAK,MAAM,IAAI,IAAI,EAAE,EAAE,CAAC;QACtB,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QAC7F,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;SACxB,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE;QAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;QACtD,MAAM,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,EAAE,IAAI,CAAC,CAAC;QAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;IAC1F,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,OAAO,CAAC,KAAwB,EAAE,GAAG,GAAG,CAAC;IAChD,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QACvB,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC;IACvF,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC7C,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;AACjF,CAAC;AAED,gFAAgF;AAChF,SAAS,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAqD;IAC/E,MAAM,KAAK,GACT,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC;IAEhF,OAAO,CACL,cAAI,SAAS,EAAC,SAAS,aACrB,eAAM,SAAS,EAAC,oBAAoB,YAAE,KAAK,GAAQ,EACnD,eAAM,SAAS,EAAC,aAAa,EAAC,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,EAAE,YACzF,eACE,SAAS,EAAC,eAAe,EACzB,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,GAAG,CAAC,GAAG,EAAE,GACjE,GACG,EACP,gBAAM,SAAS,EAAC,oBAAoB,aACjC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IACrC,IACJ,CACN,CAAC;AACJ,CAAC","sourcesContent":["/**\n * What this build is a build *of*, and how far the edit landed from itself.\n *\n * A docket names components. It never named the commit those components are a\n * consequence of, and a reviewer arriving at a build had the one thing they could\n * not get from the pixels — the diff — sitting in the store, unprinted. Three\n * facts, in the order somebody reads them:\n *\n * - **The commits.** A build is one commit, and a build following another build\n * is a range. The range is the accumulation: two builds apart, the changes on\n * this page are the consequence of everything between them, and a single hash\n * at the top of the screen invites the reader to blame the last one.\n * - **The files.** `reach.changed` is the diff, already read, already stored. It\n * is the shortest answer to *why did anything move at all*, and until now it\n * was reachable only from a second page.\n * - **How far it went.** Depth from an edited file to a component that moved.\n *\n * The third is the one worth the space. A commit whose movement is all at depth\n * zero did what it said; a commit with eleven components moving three imports out\n * changed something shared, and that is the difference between an afternoon and a\n * revert. It is measurable here and nowhere else on the surface — every other\n * page is scoped to one change, and depth is a property of the whole spread.\n */\n\nimport type { ReactElement } from 'react';\nimport type { BuildDetail } from '../review-types.js';\nimport { attributionsOf } from './attribution.js';\nimport { Marked } from './because.js';\nimport type { Crossing } from './crossing.js';\nimport { spreadOf, type Depth, type Unplaced } from './distance.js';\nimport { heldBy, type Enclosure, type Holding } from './holding.js';\nimport { count, number } from './text.js';\n\n/** How many paths are printed before the rest become a count. */\nconst NAMED = 10;\n\nexport function Impact({\n build,\n crossing,\n}: {\n readonly build: BuildDetail;\n readonly crossing: Crossing;\n}): ReactElement {\n const earlier = crossing.state === 'ready' ? crossing.earlier : undefined;\n const since = earlier?.commit === build.commit ? undefined : earlier;\n\n return (\n <section className=\"va-impact\">\n <p className=\"va-impact-commit\">\n {since === undefined ? null : (\n <>\n <code className=\"va-commit\">{since.commit.slice(0, 8)}</code>\n <span className=\"va-note\"> … </span>\n </>\n )}\n <code className=\"va-commit\">{build.commit.slice(0, 8)}</code>\n {build.branch === undefined ? null : <span className=\"va-note\"> on {build.branch}</span>}\n {since === undefined ? null : (\n <span className=\"va-note\">\n {' '}\n — everything since build {since.build}, not one commit\n </span>\n )}\n </p>\n\n <Changed build={build} />\n <Spread build={build} />\n </section>\n );\n}\n\n/** The diff itself: what the commit touched, and against what. */\nfunction Changed({ build }: { readonly build: BuildDetail }): ReactElement {\n const { reach } = build;\n\n if (reach === null) {\n return (\n <p className=\"va-note\">\n No diff was read for this build, so nothing here can say which files it came from.\n </p>\n );\n }\n\n if (reach.whole !== undefined) {\n return (\n <p className=\"va-note\">\n The diff against <code>{reach.against}</code> could not be attributed: {reach.whole}\n </p>\n );\n }\n\n const named = reach.changed.slice(0, NAMED);\n const rest = reach.changed.length - named.length;\n\n return (\n <div className=\"va-impact-files\">\n <p className=\"va-note\">\n {count(reach.changed.length, 'file')} changed against <code>{reach.against}</code>\n </p>\n <ul title={reach.changed.join('\\n')}>\n {named.map((file) => (\n <li key={file}>\n <code>{file}</code>\n </li>\n ))}\n {rest === 0 ? null : <li className=\"va-note\">and {count(rest, 'more file')}</li>}\n </ul>\n </div>\n );\n}\n\n/**\n * The impact-depth histogram, and the one sentence derived from it.\n *\n * The sentence is the deepest rung anything moved on, because that is the number\n * a reviewer would otherwise have to read off the bars themselves — and a chart\n * whose headline the reader has to compute is a chart that gets skipped.\n */\nfunction Spread({ build }: { readonly build: BuildDetail }): ReactElement | null {\n const spread = spreadOf(build);\n if (spread === null || spread.rungs.length === 0) return null;\n\n const deepest = [...spread.rungs].reverse().find((rung) => rung.moved > 0);\n const widest = Math.max(...spread.rungs.map((rung) => rung.reached));\n\n return (\n <div className=\"va-impact-depth\">\n <h2>How far the edit landed</h2>\n <p className=\"va-note\">\n {deepest === undefined\n ? 'Nothing the commit declares moved.'\n : deepest.depth === 0\n ? 'Movement in what the commit declares stops at the files it changed.'\n : `Movement reaches ${count(deepest.depth, 'import')} out from an edited file.`}\n </p>\n <ul className=\"va-rungs\">\n {spread.rungs.map((rung) => (\n <Rung key={rung.depth} rung={rung} widest={widest} />\n ))}\n </ul>\n <Undeclared build={build} moved={spread.undeclared} />\n {spread.throughUnread === 0 ? null : (\n <p className=\"va-note\">\n {count(spread.throughUnread, 'component')} reached only through a file the scan could not\n read, so their depth is measured from the blind spot rather than from the edit — they are\n not counted above.\n </p>\n )}\n </div>\n );\n}\n\n/**\n * The movement the histogram cannot hold, and the reason for each of it.\n *\n * Depth is measured from an edited file, so a component no changed file declares\n * has nothing to be measured from and drops out of every bar above. This said *no\n * rung above holds them* and stopped there, which restates the axis instead of\n * answering the question — and it was answerable twice over, because the run had\n * already climbed the other direction and written a sentence per movement.\n *\n * So the run's own sentence is what this prints. The census walk in\n * [`holding.ts`](./holding.js) is kept underneath it and used only where the\n * store holds no attribution — a build ingested before they were carried, or a\n * run that composed no census — and every state it can return still gets its own\n * clause, because a component nothing draws and a component nobody measured are\n * different facts.\n */\nfunction Undeclared({\n build,\n moved,\n}: {\n readonly build: BuildDetail;\n readonly moved: readonly Unplaced[];\n}): ReactElement | null {\n if (moved.length === 0) return null;\n const held = heldBy(build);\n const attributed = attributionsOf(build);\n\n return (\n <div className=\"va-unheld\">\n <p>\n {/* The clause after the dash is a cap, not an explanation: these are not\n in the bars, and a reader who is not told that reads the histogram as\n the whole of the movement. */}\n {count(moved.length, 'component')} no file in this commit declares — not in the bars above:\n </p>\n <ul>\n {moved.map((each) => {\n const recorded = attributed.about(each.component)[0];\n return (\n <li key={each.component} title={each.subjects.join('\\n')}>\n <span className=\"va-unheld-name\">{each.component}</span>\n <span className=\"va-unheld-why\">\n {recorded === undefined ? (\n drawnBy(held(each.component, each.subjects))\n ) : (\n <Marked say={recorded.because} />\n )}\n </span>\n </li>\n );\n })}\n </ul>\n </div>\n );\n}\n\n/** Who draws it, as the clause that follows its name. */\nfunction drawnBy(holding: Holding): string {\n switch (holding.kind) {\n case 'unrecorded':\n return 'this run recorded no composition, so nothing here knows what draws it';\n case 'unlisted':\n return 'the run drew no boundary under this name, so nothing here knows what draws it';\n case 'outermost':\n return 'nothing in the suite draws it — wherever it appears, it is the outermost component';\n case 'enclosed':\n return `drawn by ${listing(holding.within)}, and the commit reaches none of them`;\n case 'unmeasured':\n return `drawn by ${listing(holding.within)}, and no diff was read to place them against`;\n case 'through':\n return chains(holding.by);\n }\n}\n\n/**\n * The chains, one clause each, grouped by the path they take.\n *\n * Two holders that draw a component the same way are one sentence — *MainNav and\n * NavItem draw it* — because the reviewer is being told a route, and printing\n * the route twice under two names is the same route twice.\n */\nfunction chains(by: readonly Enclosure[]): string {\n const routes = new Map<string, { through: readonly string[]; holders: string[] }>();\n for (const each of by) {\n const route = routes.get(each.through.join(' > ')) ?? { through: each.through, holders: [] };\n route.holders.push(each.holder);\n routes.set(each.through.join(' > '), route);\n }\n\n return [...routes.values()]\n .map(({ through, holders }) => {\n const draws = holders.length === 1 ? 'draws' : 'draw';\n const [first, ...rest] = [...through, 'it'];\n const tail = rest.map((name) => `which draws ${name}`).join(', ');\n return `${listing(holders)} ${draws} ${first ?? 'it'}${tail === '' ? '' : `, ${tail}`}`;\n })\n .join('; ');\n}\n\n/**\n * Names as a reader would say them, and a count once there are too many to say.\n *\n * The cap is on the enclosure list, which is suite-wide: a `Card` inside forty\n * things would otherwise put forty names in a sentence whose point is that none\n * of them is the one the reviewer wants.\n */\nfunction listing(names: readonly string[], cap = 3): string {\n if (names.length > cap) {\n return `${names.slice(0, cap).join(', ')} and ${count(names.length - cap, 'other')}`;\n }\n if (names.length <= 1) return names.join('');\n return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1] ?? ''}`;\n}\n\n/** One depth: everything the commit reaches there, and how much of it moved. */\nfunction Rung({ rung, widest }: { readonly rung: Depth; readonly widest: number }): ReactElement {\n const label =\n rung.depth === 0 ? 'in a changed file' : `${count(rung.depth, 'import')} out`;\n\n return (\n <li className=\"va-rung\">\n <span className=\"va-rung-at va-note\">{label}</span>\n <span className=\"va-rung-bar\" style={{ width: `${String((rung.reached / widest) * 100)}%` }}>\n <span\n className=\"va-rung-moved\"\n style={{ width: `${String((rung.moved / rung.reached) * 100)}%` }}\n />\n </span>\n <span className=\"va-rung-num va-num\">\n {number(rung.moved)}/{number(rung.reached)}\n </span>\n </li>\n );\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review surface: React components, a JSON client, and a stylesheet as text.
|
|
3
|
+
*
|
|
4
|
+
* Requires React, and nothing else — no router, no data library, no CSS
|
|
5
|
+
* toolchain. Separate from every other entrypoint so that a Worker serving
|
|
6
|
+
* baselines does not pull React into its bundle to do it.
|
|
7
|
+
*
|
|
8
|
+
* The addresses are part of the surface. {@link ReviewApp} routes itself against
|
|
9
|
+
* `location` when it is left alone, which is what the Node service wants; a host
|
|
10
|
+
* with a router of its own passes `route` and `onNavigate` and keeps the URL,
|
|
11
|
+
* and {@link parseRoute}, {@link hrefOf} and {@link isPagePath} are the three
|
|
12
|
+
* functions that takes. The last one is why a link that renders is a link that
|
|
13
|
+
* serves: the host answers exactly the paths this surface will ever produce.
|
|
14
|
+
*/
|
|
15
|
+
export { BuildList, ChangelogEntries, ChangelogPage, ChurnLine, CoverageLine, OriginsPanel, RegionOverlay, RegionTable, ReviewApp, StabilityLine, SubjectHistory, SubjectPanel, Variations, Viewer, modesFor, originsOf, type Appearance, type Origin, type Origins, type ReviewAppProps, type ViewerMode, } from './review.js';
|
|
16
|
+
export { hrefOf, isPagePath, parseRoute, type Order, type Route, } from './route.js';
|
|
17
|
+
export { DeclarationsPanel } from './declarations.js';
|
|
18
|
+
export { Settled, needsReview } from './settled.js';
|
|
19
|
+
export { ReviewRequestError, createReviewClient, type ReviewClient, type ReviewClientOptions, } from './client.js';
|
|
20
|
+
export { REVIEW_STYLES } from './styles.js';
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review surface: React components, a JSON client, and a stylesheet as text.
|
|
3
|
+
*
|
|
4
|
+
* Requires React, and nothing else — no router, no data library, no CSS
|
|
5
|
+
* toolchain. Separate from every other entrypoint so that a Worker serving
|
|
6
|
+
* baselines does not pull React into its bundle to do it.
|
|
7
|
+
*
|
|
8
|
+
* The addresses are part of the surface. {@link ReviewApp} routes itself against
|
|
9
|
+
* `location` when it is left alone, which is what the Node service wants; a host
|
|
10
|
+
* with a router of its own passes `route` and `onNavigate` and keeps the URL,
|
|
11
|
+
* and {@link parseRoute}, {@link hrefOf} and {@link isPagePath} are the three
|
|
12
|
+
* functions that takes. The last one is why a link that renders is a link that
|
|
13
|
+
* serves: the host answers exactly the paths this surface will ever produce.
|
|
14
|
+
*/
|
|
15
|
+
export { BuildList, ChangelogEntries, ChangelogPage, ChurnLine, CoverageLine, OriginsPanel, RegionOverlay, RegionTable, ReviewApp, StabilityLine, SubjectHistory, SubjectPanel, Variations, Viewer, modesFor, originsOf, } from './review.js';
|
|
16
|
+
export { hrefOf, isPagePath, parseRoute, } from './route.js';
|
|
17
|
+
export { DeclarationsPanel } from './declarations.js';
|
|
18
|
+
export { Settled, needsReview } from './settled.js';
|
|
19
|
+
export { ReviewRequestError, createReviewClient, } from './client.js';
|
|
20
|
+
export { REVIEW_STYLES } from './styles.js';
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,WAAW,EACX,SAAS,EACT,aAAa,EACb,cAAc,EACd,YAAY,EACZ,UAAU,EACV,MAAM,EACN,QAAQ,EACR,SAAS,GAMV,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,MAAM,EACN,UAAU,EACV,UAAU,GAGX,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EACL,kBAAkB,EAClB,kBAAkB,GAGnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC","sourcesContent":["/**\n * The review surface: React components, a JSON client, and a stylesheet as text.\n *\n * Requires React, and nothing else — no router, no data library, no CSS\n * toolchain. Separate from every other entrypoint so that a Worker serving\n * baselines does not pull React into its bundle to do it.\n *\n * The addresses are part of the surface. {@link ReviewApp} routes itself against\n * `location` when it is left alone, which is what the Node service wants; a host\n * with a router of its own passes `route` and `onNavigate` and keeps the URL,\n * and {@link parseRoute}, {@link hrefOf} and {@link isPagePath} are the three\n * functions that takes. The last one is why a link that renders is a link that\n * serves: the host answers exactly the paths this surface will ever produce.\n */\n\nexport {\n BuildList,\n ChangelogEntries,\n ChangelogPage,\n ChurnLine,\n CoverageLine,\n OriginsPanel,\n RegionOverlay,\n RegionTable,\n ReviewApp,\n StabilityLine,\n SubjectHistory,\n SubjectPanel,\n Variations,\n Viewer,\n modesFor,\n originsOf,\n type Appearance,\n type Origin,\n type Origins,\n type ReviewAppProps,\n type ViewerMode,\n} from './review.js';\nexport {\n hrefOf,\n isPagePath,\n parseRoute,\n type Order,\n type Route,\n} from './route.js';\nexport { DeclarationsPanel } from './declarations.js';\nexport { Settled, needsReview } from './settled.js';\nexport {\n ReviewRequestError,\n createReviewClient,\n type ReviewClient,\n type ReviewClientOptions,\n} from './client.js';\nexport { REVIEW_STYLES } from './styles.js';\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The leading cause of a subject, written once so four readings cannot disagree.
|
|
3
|
+
*
|
|
4
|
+
* The rule: the **first** region the report marked as a cause, never the largest.
|
|
5
|
+
* Ranking by area names the container that reflowed instead of the edit that moved
|
|
6
|
+
* it — measured at 6× on one change — and the report already ordered its regions
|
|
7
|
+
* by what it believed caused them.
|
|
8
|
+
*
|
|
9
|
+
* It was written out four times before this: the docket filed a subject under one
|
|
10
|
+
* component, the rail labelled it, the record queried it, and the map read it. All
|
|
11
|
+
* four agreed by coincidence and by review. A rule reimplemented per caller is one
|
|
12
|
+
* edit away from a page that files a subject under one name and explains it under
|
|
13
|
+
* another, and that is the failure nobody would see — both halves stay plausible.
|
|
14
|
+
*/
|
|
15
|
+
import type { RegionRecord } from '@variance-authority/report';
|
|
16
|
+
/** The region the report blamed, or `undefined` when it blamed none. */
|
|
17
|
+
export declare function leadOf(subject: {
|
|
18
|
+
readonly regions: readonly RegionRecord[];
|
|
19
|
+
}): RegionRecord | undefined;
|
|
20
|
+
/** The component that region names, when it named one. */
|
|
21
|
+
export declare function causeOf(subject: {
|
|
22
|
+
readonly regions: readonly RegionRecord[];
|
|
23
|
+
}): string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* The shape of the leading difference, when the run recorded one.
|
|
26
|
+
*
|
|
27
|
+
* Absent is not *the same shape as something else*. Two runs whose leading
|
|
28
|
+
* regions both recorded nothing are two runs that said nothing, and a comparison
|
|
29
|
+
* treating that as a match would report *the identical difference again* about a
|
|
30
|
+
* pair nothing measured.
|
|
31
|
+
*/
|
|
32
|
+
export declare function shapeOf(subject: {
|
|
33
|
+
readonly regions: readonly RegionRecord[];
|
|
34
|
+
}): string | undefined;
|
|
35
|
+
//# sourceMappingURL=lead.d.ts.map
|
package/dist/ui/lead.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The leading cause of a subject, written once so four readings cannot disagree.
|
|
3
|
+
*
|
|
4
|
+
* The rule: the **first** region the report marked as a cause, never the largest.
|
|
5
|
+
* Ranking by area names the container that reflowed instead of the edit that moved
|
|
6
|
+
* it — measured at 6× on one change — and the report already ordered its regions
|
|
7
|
+
* by what it believed caused them.
|
|
8
|
+
*
|
|
9
|
+
* It was written out four times before this: the docket filed a subject under one
|
|
10
|
+
* component, the rail labelled it, the record queried it, and the map read it. All
|
|
11
|
+
* four agreed by coincidence and by review. A rule reimplemented per caller is one
|
|
12
|
+
* edit away from a page that files a subject under one name and explains it under
|
|
13
|
+
* another, and that is the failure nobody would see — both halves stay plausible.
|
|
14
|
+
*/
|
|
15
|
+
/** The region the report blamed, or `undefined` when it blamed none. */
|
|
16
|
+
export function leadOf(subject) {
|
|
17
|
+
return subject.regions.find((region) => region.cause === true);
|
|
18
|
+
}
|
|
19
|
+
/** The component that region names, when it named one. */
|
|
20
|
+
export function causeOf(subject) {
|
|
21
|
+
return leadOf(subject)?.component;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The shape of the leading difference, when the run recorded one.
|
|
25
|
+
*
|
|
26
|
+
* Absent is not *the same shape as something else*. Two runs whose leading
|
|
27
|
+
* regions both recorded nothing are two runs that said nothing, and a comparison
|
|
28
|
+
* treating that as a match would report *the identical difference again* about a
|
|
29
|
+
* pair nothing measured.
|
|
30
|
+
*/
|
|
31
|
+
export function shapeOf(subject) {
|
|
32
|
+
return leadOf(subject)?.fingerprint;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=lead.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lead.js","sourceRoot":"","sources":["../../src/ui/lead.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,wEAAwE;AACxE,MAAM,UAAU,MAAM,CAAC,OAEtB;IACC,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;AACjE,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,OAAO,CAAC,OAAsD;IAC5E,OAAO,MAAM,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;AACpC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,OAAsD;IAC5E,OAAO,MAAM,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC;AACtC,CAAC","sourcesContent":["/**\n * The leading cause of a subject, written once so four readings cannot disagree.\n *\n * The rule: the **first** region the report marked as a cause, never the largest.\n * Ranking by area names the container that reflowed instead of the edit that moved\n * it — measured at 6× on one change — and the report already ordered its regions\n * by what it believed caused them.\n *\n * It was written out four times before this: the docket filed a subject under one\n * component, the rail labelled it, the record queried it, and the map read it. All\n * four agreed by coincidence and by review. A rule reimplemented per caller is one\n * edit away from a page that files a subject under one name and explains it under\n * another, and that is the failure nobody would see — both halves stay plausible.\n */\n\nimport type { RegionRecord } from '@variance-authority/report';\n\n/** The region the report blamed, or `undefined` when it blamed none. */\nexport function leadOf(subject: {\n readonly regions: readonly RegionRecord[];\n}): RegionRecord | undefined {\n return subject.regions.find((region) => region.cause === true);\n}\n\n/** The component that region names, when it named one. */\nexport function causeOf(subject: { readonly regions: readonly RegionRecord[] }): string | undefined {\n return leadOf(subject)?.component;\n}\n\n/**\n * The shape of the leading difference, when the run recorded one.\n *\n * Absent is not *the same shape as something else*. Two runs whose leading\n * regions both recorded nothing are two runs that said nothing, and a comparison\n * treating that as a match would report *the identical difference again* about a\n * pair nothing measured.\n */\nexport function shapeOf(subject: { readonly regions: readonly RegionRecord[] }): string | undefined {\n return leadOf(subject)?.fingerprint;\n}\n"]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Looking at a change from the card that decides it.
|
|
3
|
+
*
|
|
4
|
+
* The docket answers *what moved and why*, and then offers two buttons. That is
|
|
5
|
+
* one question short: a reviewer approving a restyle of `Button` across eleven
|
|
6
|
+
* renders has been told the name of the component, the size of the difference and
|
|
7
|
+
* the shape it repeats in, and has not been shown the difference. Sending them to
|
|
8
|
+
* the full stage for that is sending them away from the decision — a route
|
|
9
|
+
* capture is nine thousand pixels tall, the difference is a hundred and thirty
|
|
10
|
+
* across, and finding it is a scroll they perform once per subject.
|
|
11
|
+
*
|
|
12
|
+
* So the change is brought to the card, cropped to where the run measured it.
|
|
13
|
+
*
|
|
14
|
+
* ## One crop per shape, not one per place
|
|
15
|
+
*
|
|
16
|
+
* The card's own argument is *one edit, N places*. A strip of eleven thumbnails
|
|
17
|
+
* would contradict it in the same breath as making it, and would ask the browser
|
|
18
|
+
* to decode eleven full-page captures to say a thing the fingerprints already
|
|
19
|
+
* said. What is drawn instead is one crop per distinct shape, which is exactly
|
|
20
|
+
* the set the card claims is redundant — if two of them look different, the claim
|
|
21
|
+
* was wrong and the reviewer can see that it was.
|
|
22
|
+
*
|
|
23
|
+
* An appearance whose run recorded no shape is its own crop. Absent is not equal:
|
|
24
|
+
* two differences nothing characterised have not been shown to match, and folding
|
|
25
|
+
* them together would hide one behind a picture of the other.
|
|
26
|
+
*
|
|
27
|
+
* ## The difference is the first layer
|
|
28
|
+
*
|
|
29
|
+
* The strip opens on the mask the comparison drew. A crop of the candidate is a
|
|
30
|
+
* picture of the component, and a reviewer reading one against a baseline they
|
|
31
|
+
* remember is redoing by eye the comparison the run already did to the pixel. The
|
|
32
|
+
* two readings are one press behind it and stay mounted once asked for, so moving
|
|
33
|
+
* between the three is a repaint rather than a fetch.
|
|
34
|
+
*
|
|
35
|
+
* ## Deferred by the window, not by a button
|
|
36
|
+
*
|
|
37
|
+
* A route candidate here is 1280 × 9000 and costs about 46MB of bitmap to decode,
|
|
38
|
+
* and a change with six shapes asks for six of them. That cost is why the strip
|
|
39
|
+
* used to sit behind a *look at the change* button — which priced every difference
|
|
40
|
+
* on the page at a click, and there is no reviewer who does not want to see the
|
|
41
|
+
* difference. It also capped the strip at three, so a change with six shapes drew
|
|
42
|
+
* half of itself and counted the rest.
|
|
43
|
+
*
|
|
44
|
+
* So the deferral moved to where the cost is. Each crop asks for its raster when
|
|
45
|
+
* its own window comes near the viewport, and only for the layer being shown.
|
|
46
|
+
* `loading="lazy"` cannot do that job here: the plate is the whole capture
|
|
47
|
+
* translated so the region lands in a 264px window, which on a mobile route is a
|
|
48
|
+
* 17,000px offset — the browser sees a box far outside the viewport and defers it
|
|
49
|
+
* forever, leaving a red ring over an empty frame. What is watched instead is the
|
|
50
|
+
* window, which is 264 × 176 and sitting exactly where the reviewer is looking.
|
|
51
|
+
*/
|
|
52
|
+
import { type ReactElement } from 'react';
|
|
53
|
+
import type { RegionRecord } from '@variance-authority/report';
|
|
54
|
+
import type { ReviewClient } from './client.js';
|
|
55
|
+
import type { Appearance } from './grouping.js';
|
|
56
|
+
import { type Size } from './viewer.js';
|
|
57
|
+
/** Where the plate sits inside the window, and at what magnification. */
|
|
58
|
+
export interface Frame {
|
|
59
|
+
readonly scale: number;
|
|
60
|
+
/** The plate's offset inside the window, in CSS pixels. Usually negative. */
|
|
61
|
+
readonly left: number;
|
|
62
|
+
readonly top: number;
|
|
63
|
+
/** `false` when the region is bigger than the window even pushed all the way out. */
|
|
64
|
+
readonly whole: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The magnification and offset that put one region in the middle of the window.
|
|
68
|
+
*
|
|
69
|
+
* The clamp on the second axis is what keeps the crop a picture rather than a
|
|
70
|
+
* coordinate: a region near the top of a capture, centred honestly, would put
|
|
71
|
+
* half the window above the first row of the image, and a reviewer would be
|
|
72
|
+
* looking at a difference with a band of nothing over it. So the plate is allowed
|
|
73
|
+
* to be off-centre when the region is near an edge, and is centred outright when
|
|
74
|
+
* the whole capture is smaller than the window — which is most stories.
|
|
75
|
+
*/
|
|
76
|
+
export declare function frameOf(region: RegionRecord, box: Size, window: Size): Frame;
|
|
77
|
+
/** One difference worth drawing, and how many places carry the same one. */
|
|
78
|
+
export interface Sighting {
|
|
79
|
+
readonly appearance: Appearance;
|
|
80
|
+
readonly at: RegionRecord;
|
|
81
|
+
/** Places carrying this exact shape. `1` when the run recorded none. */
|
|
82
|
+
readonly shared: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Which of the appearances get a picture.
|
|
86
|
+
*
|
|
87
|
+
* Report order within a shape, so the crop shown is the one the docket lists
|
|
88
|
+
* first rather than the biggest — the same rule the leading cause is chosen by,
|
|
89
|
+
* and for the same reason: largest-first names the container that reflowed.
|
|
90
|
+
*/
|
|
91
|
+
export declare function sightings(appearances: readonly Appearance[]): readonly Sighting[];
|
|
92
|
+
/**
|
|
93
|
+
* The strip, and the one control on it.
|
|
94
|
+
*
|
|
95
|
+
* The switch is card-level rather than per crop, because the question a reviewer
|
|
96
|
+
* asks is *did all of these move the same way* and answering it means moving them
|
|
97
|
+
* together. A layer that has been asked for once stays mounted and switches
|
|
98
|
+
* opacity, so returning to it does not go through a frame of nothing — a flash
|
|
99
|
+
* reads as a third state, and on a difference of twenty pixels it is the loudest
|
|
100
|
+
* thing on screen.
|
|
101
|
+
*/
|
|
102
|
+
export declare function Look({ client, build, component, appearances, onOpen, }: {
|
|
103
|
+
readonly client: ReviewClient;
|
|
104
|
+
readonly build: string;
|
|
105
|
+
readonly component: string;
|
|
106
|
+
readonly appearances: readonly Appearance[];
|
|
107
|
+
readonly onOpen?: ((subject: string) => void) | undefined;
|
|
108
|
+
}): ReactElement | null;
|
|
109
|
+
//# sourceMappingURL=look.d.ts.map
|