@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,149 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { unforeseen } from './foreseen.js';
|
|
3
|
+
import { originsOf, shapesOf, sourceOf } from './grouping.js';
|
|
4
|
+
import { docketOf, ORDERS } from './order.js';
|
|
5
|
+
import { RootHeading } from './root.js';
|
|
6
|
+
import { Go } from './shell.js';
|
|
7
|
+
import { senseOfSubject } from './sense.js';
|
|
8
|
+
import { count, magnitude, number } from './text.js';
|
|
9
|
+
export function OriginsPanel({ build, crossing, order, selected, go, }) {
|
|
10
|
+
const { origins, unattributed } = originsOf(build);
|
|
11
|
+
const renders = origins.reduce((total, origin) => total + origin.appearances.length, 0);
|
|
12
|
+
if (origins.length === 0 && unattributed.length === 0) {
|
|
13
|
+
return (_jsx("div", { className: "va-rail-empty va-note", children: "Nothing in this build changed, so there is nothing to approve." }));
|
|
14
|
+
}
|
|
15
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "va-rail-head", children: [_jsxs("p", { className: "va-rail-tally", children: [count(origins.length, 'change'), " \u00B7 ", count(renders, 'render')] }), _jsx(Sorting, { order: order, build: build.build, go: go })] }), _jsx(Unreached, { build: build, go: go }), _jsxs("div", { className: "va-rail-list va-scroll", children: [docketOf(build, origins, order).map((group) => (_jsx(Band, { group: group, detail: build, build: build.build, crossing: crossing, selected: selected, go: go }, group.lane + group.title))), unattributed.length === 0 ? null : (_jsx(Unattributed, { subjects: unattributed, build: build.build, go: go }))] })] }));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether anything moved that the commit cannot reach.
|
|
19
|
+
*
|
|
20
|
+
* The empty case is printed, and it is the only place on this surface where an
|
|
21
|
+
* empty list is worth a sentence: *nothing moved outside what you edited* is the
|
|
22
|
+
* strongest thing a build can say, a reviewer cannot derive it from a docket of
|
|
23
|
+
* changes that are all accounted for, and its absence is what makes the other
|
|
24
|
+
* case legible when it happens.
|
|
25
|
+
*
|
|
26
|
+
* Silent when the run walked no diff. Then there is no prediction to have kept.
|
|
27
|
+
*/
|
|
28
|
+
function Unreached({ build, go, }) {
|
|
29
|
+
const loose = unforeseen(build);
|
|
30
|
+
if (loose === null)
|
|
31
|
+
return null;
|
|
32
|
+
if (loose.length === 0) {
|
|
33
|
+
return _jsx("p", { className: "va-foreseen va-note", children: "Everything that moved is under a file this commit changed." });
|
|
34
|
+
}
|
|
35
|
+
return (_jsxs("p", { className: "va-foreseen va-note va-foreseen-off", children: [_jsx("span", { className: "va-num", children: number(loose.length) }), " moved with nothing in this commit above", ' ', loose.slice(0, 3).map((subject, index) => (_jsxs("span", { children: [index === 0 ? '— ' : ', ', _jsx(Go, { to: { page: 'subject', build: build.build, subject }, go: go, className: "va-from-go", children: subject })] }, subject))), loose.length > 3 ? ` and ${number(loose.length - 3)} more` : ''] }));
|
|
36
|
+
}
|
|
37
|
+
/** The order picker: four addresses, one of which is the one you are at. */
|
|
38
|
+
function Sorting({ order, build, go, }) {
|
|
39
|
+
return (_jsx("div", { className: "va-sorting", role: "group", "aria-label": "Order the changes", children: ORDERS.map(({ order: each, label, why }) => (_jsx(Go, { to: { page: 'build', build, order: each }, go: go, className: each === order ? 'va-sort va-on' : 'va-sort', title: why, children: label }, each))) }));
|
|
40
|
+
}
|
|
41
|
+
/** One band: the claim, the count, the rows. */
|
|
42
|
+
function Band({ group, detail, build, crossing, selected, go, }) {
|
|
43
|
+
return (_jsxs("section", { className: `va-band va-band-${group.lane}`, children: [_jsx(RootHeading, { group: group, detail: detail, go: go }), _jsx("ul", { children: group.changes.map(({ origin, of }) => (_jsx(Row, { origin: origin, ...(of === undefined ? {} : { of }), ...(group.root === undefined ? {} : { from: group.root.name }), build: build, crossing: crossing, open: origin.component === selected, go: go }, origin.component))) })] }));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* One change, in a row a reviewer can scan a column of.
|
|
47
|
+
*
|
|
48
|
+
* Four facts and at most one mark. The fingerprint is not one of them — it is an
|
|
49
|
+
* identity, not a finding, and it was on the third line of every card in the
|
|
50
|
+
* version of this surface that told nobody anything. What replaces it is the
|
|
51
|
+
* count of distinct shapes, which is the part of the same record a reviewer can
|
|
52
|
+
* act on: one shape across eleven renders is one decision, and nine shapes across
|
|
53
|
+
* eleven is a component whose renders each absorbed the edit their own way.
|
|
54
|
+
*
|
|
55
|
+
* The fourth fact is the source, and it is fourth rather than absent because the
|
|
56
|
+
* band above it no longer says anything about where these came from. `CardFooter`
|
|
57
|
+
* sat under *something you edited hands these what they draw* for a build and a
|
|
58
|
+
* half with `ProductCard` in the store the whole time.
|
|
59
|
+
*/
|
|
60
|
+
function Row({ origin, of, from, build, crossing, open, go, }) {
|
|
61
|
+
const shapes = shapesOf(origin.appearances).size;
|
|
62
|
+
const decided = origin.appearances.filter(({ subject }) => subject.decision !== null).length;
|
|
63
|
+
return (_jsx("li", { className: open ? 'va-row va-here' : 'va-row', children: _jsxs(Go, { to: { page: 'change', build, change: origin.component }, go: go, className: "va-row-link", title: origin.file ?? origin.component, children: [_jsx("span", { className: "va-row-name", children: origin.component }), _jsxs("span", { className: "va-row-spread va-note", children: [of === undefined
|
|
64
|
+
? count(origin.appearances.length, 'render')
|
|
65
|
+
: `${number(of)} of ${count(origin.appearances.length, 'render')}`, shapes > 1 ? ` · ${count(shapes, 'shape')}` : ''] }), _jsxs("span", { className: "va-row-size va-num", children: [number(origin.pixels), " px"] }), _jsx(RowMark, { origin: origin, crossing: crossing, decided: decided }), _jsx(Source, { origin: origin, ...(from === undefined ? {} : { from }) })] }) }));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Where the change came from, by name.
|
|
69
|
+
*
|
|
70
|
+
* Nothing is derived here and nothing is phrased: [`sourceOf`](./grouping.js)
|
|
71
|
+
* reads the owner, the file or the token off the movement the run wrote, and
|
|
72
|
+
* this prints the list. A row with no source line is a row the run recorded
|
|
73
|
+
* nothing for and the docket has no file for either — which is a fact about the
|
|
74
|
+
* record, and is drawn as the empty space it is rather than as a sentence
|
|
75
|
+
* apologising for it.
|
|
76
|
+
*/
|
|
77
|
+
function Source({ origin, from, }) {
|
|
78
|
+
// Minus what the heading over it already said. Under `ui/button.tsx`, an
|
|
79
|
+
// `edited` row's source *is* `ui/button.tsx`, and printing it again spends the
|
|
80
|
+
// row's third line saying the band's name back to the reader. The rows that
|
|
81
|
+
// still have something to say — the chain an owner reaches through — keep it.
|
|
82
|
+
const names = sourceOf(origin).filter((name) => name !== from);
|
|
83
|
+
if (names.length === 0)
|
|
84
|
+
return null;
|
|
85
|
+
const say = names.join(', ');
|
|
86
|
+
// Ellipsised at the rail's width, so the full text has to be somewhere. This
|
|
87
|
+
// is the one thing a `title` is for: the same string, not a different one.
|
|
88
|
+
return (_jsx("span", { className: "va-row-from", title: say, children: say }));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The one thing worth knowing before opening this change, or nothing.
|
|
92
|
+
*
|
|
93
|
+
* At most one, and in this order, because a row with four badges on it is a row
|
|
94
|
+
* nobody reads. Already-decided outranks everything: there is no work here. Then
|
|
95
|
+
* *you have seen this*, which is the row a reviewer can skip. Then the strandings,
|
|
96
|
+
* which is the row they cannot.
|
|
97
|
+
*/
|
|
98
|
+
function RowMark({ origin, crossing, decided, }) {
|
|
99
|
+
if (decided === origin.appearances.length) {
|
|
100
|
+
return _jsx("span", { className: "va-mark va-approved", children: "decided" });
|
|
101
|
+
}
|
|
102
|
+
if (crossing.state === 'ready') {
|
|
103
|
+
const again = origin.appearances.filter(({ subject }) => crossing.of(subject.subject)?.shift === 'again').length;
|
|
104
|
+
if (again === origin.appearances.length) {
|
|
105
|
+
return (_jsxs("span", { className: "va-mark va-known", title: `Every render of this was in build ${crossing.earlier.build} too, with the same difference`, children: ["seen in ", crossing.earlier.build] }));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if ((origin.stranded ?? []).length > 0) {
|
|
109
|
+
return (_jsx("span", { className: "va-mark va-alarm", title: "The commit reaches nothing at all in some of these renders", children: "unreached" }));
|
|
110
|
+
}
|
|
111
|
+
if (decided > 0) {
|
|
112
|
+
return (_jsxs("span", { className: "va-mark va-note", children: [number(decided), "/", number(origin.appearances.length)] }));
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The changed renders no *region* claimed — which is not the same as unexplained.
|
|
118
|
+
*
|
|
119
|
+
* Their own band and never folded into one: a difference with nothing named as
|
|
120
|
+
* its cause has no change to be approved under, and putting it in somebody else's
|
|
121
|
+
* group would hand a reviewer an unrelated edit to approve it beneath.
|
|
122
|
+
*
|
|
123
|
+
* The band used to say nothing had named these. That was false on every build
|
|
124
|
+
* carrying component hashes, and false in the direction that costs the most: a
|
|
125
|
+
* region is named by resolving the box the pixels drew, so it fails exactly when
|
|
126
|
+
* an edit reflows its neighbours and the difference merges into one blob. The
|
|
127
|
+
* hashes are still there, still per component, and they usually name the cause
|
|
128
|
+
* outright. What is missing is a *box*, not a name — so the name is on the row.
|
|
129
|
+
*/
|
|
130
|
+
function Unattributed({ subjects, build, go, }) {
|
|
131
|
+
return (_jsxs("section", { className: "va-band va-band-orphan", children: [_jsxs("h3", { children: ["No region named these ", _jsx("span", { className: "va-num", children: number(subjects.length) })] }), _jsx("ul", { children: subjects.map((subject) => (_jsx("li", { className: "va-row", children: _jsxs(Go, { to: { page: 'subject', build, subject: subject.subject }, go: go, className: "va-row-link", title: magnitude(subject), children: [_jsx("span", { className: "va-row-name", children: subject.subject }), _jsx(Blamed, { subject: subject }), _jsxs("span", { className: "va-row-size va-num", children: [number(subject.changedPixels), " px"] })] }) }, subject.subject))) })] }));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* What the hashes name in a render the regions could not.
|
|
135
|
+
*
|
|
136
|
+
* Falls back to the region count when the baseline carried no hashes, because
|
|
137
|
+
* absent is not empty: a run that never measured this has not established that
|
|
138
|
+
* nothing caused the difference, and a row printing an empty list would say it
|
|
139
|
+
* had.
|
|
140
|
+
*/
|
|
141
|
+
function Blamed({ subject }) {
|
|
142
|
+
const sense = senseOfSubject(subject);
|
|
143
|
+
const causes = sense.moved.filter((entry) => entry.cause).map((entry) => entry.component);
|
|
144
|
+
if (causes.length === 0) {
|
|
145
|
+
return (_jsx("span", { className: "va-row-spread va-note", children: count(subject.regions.length, 'region') }));
|
|
146
|
+
}
|
|
147
|
+
return (_jsxs("span", { className: "va-row-spread va-note", title: causes.join(', '), children: ["hashes name ", causes.slice(0, 2).join(', '), causes.length > 2 ? ` +${String(causes.length - 2)}` : ''] }));
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=origins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"origins.js","sourceRoot":"","sources":["../../src/ui/origins.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * The docket: every change in the build, banded, in a rail beside the one open.\n *\n * This was a stack of cards. Each carried a component, a file path, a ratio and a\n * fingerprint, and a reviewer arriving at a build of eleven changes met eleven of\n * them stacked vertically — the second one below the fold, the divergence panel\n * that would have told them they had already seen four of these nine thousand\n * pixels further down. A page cannot be both the index and the entry, and when it\n * tries the index wins the top of the screen and says nothing.\n *\n * So the list stopped carrying the story. A row says which change it is, how far\n * it went, and the single thing about it a reviewer would want before opening it:\n * that nothing reaches it, that build 5 showed it to them already, that it is\n * done. Everything else moved to [`change.tsx`](./change.tsx), where there is room\n * to say it beside the picture.\n *\n * ## The bands are the order\n *\n * A list of changes sorted by pixels answers *which of these is biggest*, and\n * nobody has ever needed to know that. The first band is the changes nothing in\n * the commit reaches — the ones that are either a real regression or a gap in the\n * graph, and either way the ones worth the reviewer's first hour. Then the ones\n * they asked for, then the ones the diff cannot name, then what is already\n * decided. Within a band, alphabetical: a reviewer looking for `Button` should\n * find it where `Button` goes, not wherever this run's pixel counts put it.\n *\n * The other orders exist because a band is a claim and a reviewer is allowed to\n * disbelieve it. [`order.ts`](./order.ts) holds them all.\n */\n\nimport type { ReactElement } from 'react';\nimport type { BuildDetail, SubjectView } from '../review-types.js';\nimport type { Crossing } from './crossing.js';\nimport { unforeseen } from './foreseen.js';\nimport { originsOf, shapesOf, sourceOf, type Origin } from './grouping.js';\nimport { docketOf, ORDERS, type Group } from './order.js';\nimport { RootHeading } from './root.js';\nimport type { Order, Route } from './route.js';\nimport { Go } from './shell.js';\nimport { senseOfSubject } from './sense.js';\nimport { count, magnitude, number } from './text.js';\n\nexport function OriginsPanel({\n build,\n crossing,\n order,\n selected,\n go,\n}: {\n readonly build: BuildDetail;\n readonly crossing: Crossing;\n readonly order: Order;\n /** The change on the stage, so the rail can say which row it is. */\n readonly selected?: string | undefined;\n readonly go: (route: Route) => void;\n}): ReactElement {\n const { origins, unattributed } = originsOf(build);\n const renders = origins.reduce((total, origin) => total + origin.appearances.length, 0);\n\n if (origins.length === 0 && unattributed.length === 0) {\n return (\n <div className=\"va-rail-empty va-note\">\n Nothing in this build changed, so there is nothing to approve.\n </div>\n );\n }\n\n return (\n <>\n <div className=\"va-rail-head\">\n <p className=\"va-rail-tally\">\n {count(origins.length, 'change')} · {count(renders, 'render')}\n </p>\n <Sorting order={order} build={build.build} go={go} />\n </div>\n\n <Unreached build={build} go={go} />\n\n <div className=\"va-rail-list va-scroll\">\n {docketOf(build, origins, order).map((group) => (\n <Band\n key={group.lane + group.title}\n group={group}\n detail={build}\n build={build.build}\n crossing={crossing}\n selected={selected}\n go={go}\n />\n ))}\n\n {unattributed.length === 0 ? null : (\n <Unattributed subjects={unattributed} build={build.build} go={go} />\n )}\n </div>\n </>\n );\n}\n\n/**\n * Whether anything moved that the commit cannot reach.\n *\n * The empty case is printed, and it is the only place on this surface where an\n * empty list is worth a sentence: *nothing moved outside what you edited* is the\n * strongest thing a build can say, a reviewer cannot derive it from a docket of\n * changes that are all accounted for, and its absence is what makes the other\n * case legible when it happens.\n *\n * Silent when the run walked no diff. Then there is no prediction to have kept.\n */\nfunction Unreached({\n build,\n go,\n}: {\n readonly build: BuildDetail;\n readonly go: (route: Route) => void;\n}): ReactElement | null {\n const loose = unforeseen(build);\n if (loose === null) return null;\n\n if (loose.length === 0) {\n return <p className=\"va-foreseen va-note\">Everything that moved is under a file this commit changed.</p>;\n }\n\n return (\n <p className=\"va-foreseen va-note va-foreseen-off\">\n <span className=\"va-num\">{number(loose.length)}</span> moved with nothing in this commit\n above{' '}\n {loose.slice(0, 3).map((subject, index) => (\n <span key={subject}>\n {index === 0 ? '— ' : ', '}\n <Go to={{ page: 'subject', build: build.build, subject }} go={go} className=\"va-from-go\">\n {subject}\n </Go>\n </span>\n ))}\n {loose.length > 3 ? ` and ${number(loose.length - 3)} more` : ''}\n </p>\n );\n}\n\n/** The order picker: four addresses, one of which is the one you are at. */\nfunction Sorting({\n order,\n build,\n go,\n}: {\n readonly order: Order;\n readonly build: string;\n readonly go: (route: Route) => void;\n}): ReactElement {\n return (\n <div className=\"va-sorting\" role=\"group\" aria-label=\"Order the changes\">\n {ORDERS.map(({ order: each, label, why }) => (\n <Go\n key={each}\n to={{ page: 'build', build, order: each }}\n go={go}\n className={each === order ? 'va-sort va-on' : 'va-sort'}\n title={why}\n >\n {label}\n </Go>\n ))}\n </div>\n );\n}\n\n/** One band: the claim, the count, the rows. */\nfunction Band({\n group,\n detail,\n build,\n crossing,\n selected,\n go,\n}: {\n readonly group: Group;\n readonly detail: BuildDetail;\n readonly build: string;\n readonly crossing: Crossing;\n readonly selected?: string | undefined;\n readonly go: (route: Route) => void;\n}): ReactElement {\n return (\n <section className={`va-band va-band-${group.lane}`}>\n <RootHeading group={group} detail={detail} go={go} />\n <ul>\n {group.changes.map(({ origin, of }) => (\n <Row\n key={origin.component}\n origin={origin}\n {...(of === undefined ? {} : { of })}\n {...(group.root === undefined ? {} : { from: group.root.name })}\n build={build}\n crossing={crossing}\n open={origin.component === selected}\n go={go}\n />\n ))}\n </ul>\n </section>\n );\n}\n\n/**\n * One change, in a row a reviewer can scan a column of.\n *\n * Four facts and at most one mark. The fingerprint is not one of them — it is an\n * identity, not a finding, and it was on the third line of every card in the\n * version of this surface that told nobody anything. What replaces it is the\n * count of distinct shapes, which is the part of the same record a reviewer can\n * act on: one shape across eleven renders is one decision, and nine shapes across\n * eleven is a component whose renders each absorbed the edit their own way.\n *\n * The fourth fact is the source, and it is fourth rather than absent because the\n * band above it no longer says anything about where these came from. `CardFooter`\n * sat under *something you edited hands these what they draw* for a build and a\n * half with `ProductCard` in the store the whole time.\n */\nfunction Row({\n origin,\n of,\n from,\n build,\n crossing,\n open,\n go,\n}: {\n readonly origin: Origin;\n /** Renders this band's cause accounts for, when it does not account for all. */\n readonly of?: number;\n /** The cause the heading above already names, so the row does not repeat it. */\n readonly from?: string;\n readonly build: string;\n readonly crossing: Crossing;\n readonly open: boolean;\n readonly go: (route: Route) => void;\n}): ReactElement {\n const shapes = shapesOf(origin.appearances).size;\n const decided = origin.appearances.filter(({ subject }) => subject.decision !== null).length;\n\n return (\n <li className={open ? 'va-row va-here' : 'va-row'}>\n <Go\n to={{ page: 'change', build, change: origin.component }}\n go={go}\n className=\"va-row-link\"\n title={origin.file ?? origin.component}\n >\n <span className=\"va-row-name\">{origin.component}</span>\n <span className=\"va-row-spread va-note\">\n {of === undefined\n ? count(origin.appearances.length, 'render')\n : `${number(of)} of ${count(origin.appearances.length, 'render')}`}\n {shapes > 1 ? ` · ${count(shapes, 'shape')}` : ''}\n </span>\n <span className=\"va-row-size va-num\">{number(origin.pixels)} px</span>\n <RowMark origin={origin} crossing={crossing} decided={decided} />\n <Source origin={origin} {...(from === undefined ? {} : { from })} />\n </Go>\n </li>\n );\n}\n\n/**\n * Where the change came from, by name.\n *\n * Nothing is derived here and nothing is phrased: [`sourceOf`](./grouping.js)\n * reads the owner, the file or the token off the movement the run wrote, and\n * this prints the list. A row with no source line is a row the run recorded\n * nothing for and the docket has no file for either — which is a fact about the\n * record, and is drawn as the empty space it is rather than as a sentence\n * apologising for it.\n */\nfunction Source({\n origin,\n from,\n}: {\n readonly origin: Origin;\n readonly from?: string;\n}): ReactElement | null {\n // Minus what the heading over it already said. Under `ui/button.tsx`, an\n // `edited` row's source *is* `ui/button.tsx`, and printing it again spends the\n // row's third line saying the band's name back to the reader. The rows that\n // still have something to say — the chain an owner reaches through — keep it.\n const names = sourceOf(origin).filter((name) => name !== from);\n if (names.length === 0) return null;\n\n const say = names.join(', ');\n // Ellipsised at the rail's width, so the full text has to be somewhere. This\n // is the one thing a `title` is for: the same string, not a different one.\n return (\n <span className=\"va-row-from\" title={say}>\n {say}\n </span>\n );\n}\n\n/**\n * The one thing worth knowing before opening this change, or nothing.\n *\n * At most one, and in this order, because a row with four badges on it is a row\n * nobody reads. Already-decided outranks everything: there is no work here. Then\n * *you have seen this*, which is the row a reviewer can skip. Then the strandings,\n * which is the row they cannot.\n */\nfunction RowMark({\n origin,\n crossing,\n decided,\n}: {\n readonly origin: Origin;\n readonly crossing: Crossing;\n readonly decided: number;\n}): ReactElement | null {\n if (decided === origin.appearances.length) {\n return <span className=\"va-mark va-approved\">decided</span>;\n }\n\n if (crossing.state === 'ready') {\n const again = origin.appearances.filter(\n ({ subject }) => crossing.of(subject.subject)?.shift === 'again',\n ).length;\n if (again === origin.appearances.length) {\n return (\n <span className=\"va-mark va-known\" title={`Every render of this was in build ${crossing.earlier.build} too, with the same difference`}>\n seen in {crossing.earlier.build}\n </span>\n );\n }\n }\n\n if ((origin.stranded ?? []).length > 0) {\n return (\n <span className=\"va-mark va-alarm\" title=\"The commit reaches nothing at all in some of these renders\">\n unreached\n </span>\n );\n }\n\n if (decided > 0) {\n return (\n <span className=\"va-mark va-note\">\n {number(decided)}/{number(origin.appearances.length)}\n </span>\n );\n }\n\n return null;\n}\n\n/**\n * The changed renders no *region* claimed — which is not the same as unexplained.\n *\n * Their own band and never folded into one: a difference with nothing named as\n * its cause has no change to be approved under, and putting it in somebody else's\n * group would hand a reviewer an unrelated edit to approve it beneath.\n *\n * The band used to say nothing had named these. That was false on every build\n * carrying component hashes, and false in the direction that costs the most: a\n * region is named by resolving the box the pixels drew, so it fails exactly when\n * an edit reflows its neighbours and the difference merges into one blob. The\n * hashes are still there, still per component, and they usually name the cause\n * outright. What is missing is a *box*, not a name — so the name is on the row.\n */\nfunction Unattributed({\n subjects,\n build,\n go,\n}: {\n readonly subjects: readonly SubjectView[];\n readonly build: string;\n readonly go: (route: Route) => void;\n}): ReactElement {\n return (\n <section className=\"va-band va-band-orphan\">\n <h3>\n No region named these <span className=\"va-num\">{number(subjects.length)}</span>\n </h3>\n <ul>\n {subjects.map((subject) => (\n <li key={subject.subject} className=\"va-row\">\n <Go\n to={{ page: 'subject', build, subject: subject.subject }}\n go={go}\n className=\"va-row-link\"\n title={magnitude(subject)}\n >\n <span className=\"va-row-name\">{subject.subject}</span>\n <Blamed subject={subject} />\n <span className=\"va-row-size va-num\">{number(subject.changedPixels)} px</span>\n </Go>\n </li>\n ))}\n </ul>\n </section>\n );\n}\n\n/**\n * What the hashes name in a render the regions could not.\n *\n * Falls back to the region count when the baseline carried no hashes, because\n * absent is not empty: a run that never measured this has not established that\n * nothing caused the difference, and a row printing an empty list would say it\n * had.\n */\nfunction Blamed({ subject }: { readonly subject: SubjectView }): ReactElement {\n const sense = senseOfSubject(subject);\n const causes = sense.moved.filter((entry) => entry.cause).map((entry) => entry.component);\n\n if (causes.length === 0) {\n return (\n <span className=\"va-row-spread va-note\">{count(subject.regions.length, 'region')}</span>\n );\n }\n\n return (\n <span className=\"va-row-spread va-note\" title={causes.join(', ')}>\n hashes name {causes.slice(0, 2).join(', ')}\n {causes.length > 2 ? ` +${String(causes.length - 2)}` : ''}\n </span>\n );\n}\n"]}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The edit, drawn as far as it got.
|
|
3
|
+
*
|
|
4
|
+
* Every other panel starts from a difference and works backwards. This one starts
|
|
5
|
+
* from the commit and works forwards: the files somebody changed, the components
|
|
6
|
+
* the import graph carries them to, and what happened on the renders those
|
|
7
|
+
* components appear in. Three records that already exist, joined on the one axis
|
|
8
|
+
* none of them carries alone — the diff knows nothing about renders, the graph
|
|
9
|
+
* knows nothing about pixels, and the comparison knows nothing about what was
|
|
10
|
+
* edited.
|
|
11
|
+
*
|
|
12
|
+
* ## Five things a file can turn out to have done
|
|
13
|
+
*
|
|
14
|
+
* Only the first is the one everybody builds for.
|
|
15
|
+
*
|
|
16
|
+
* - **the cause in N renders** — a component reached from here is named as the
|
|
17
|
+
* cause of a difference. The ordinary case.
|
|
18
|
+
* - **present, never the cause** — it renders on screens that moved, and no region
|
|
19
|
+
* in any of them names it. The screens changed for somebody else's reasons.
|
|
20
|
+
* - **nothing it reaches moved** — every render carrying it held still. An author
|
|
21
|
+
* who believed they were changing this surface finds out here, and nowhere else
|
|
22
|
+
* in this category: a comparison tool is silent about a green subject, and
|
|
23
|
+
* silence is what *worked* looks like too.
|
|
24
|
+
* - **nothing renders what it reaches** — the components exist, the graph arrives
|
|
25
|
+
* at them, and no subject in the suite draws one. That is a coverage hole
|
|
26
|
+
* reported against the edit that just walked into it, rather than against a
|
|
27
|
+
* percentage nobody reads.
|
|
28
|
+
* - **reaches no component** — a server file, a config, a test. Named rather than
|
|
29
|
+
* hidden, because a file the graph drops is the other reason a change can be
|
|
30
|
+
* invisible, and it must not look like the case above it.
|
|
31
|
+
*
|
|
32
|
+
* ## What is not claimed
|
|
33
|
+
*
|
|
34
|
+
* A render moving is not evidence that a file on it moved it. `present, never the
|
|
35
|
+
* cause` is exactly that refusal wearing a label: the edge from file to render is
|
|
36
|
+
* the import graph's, and only the region's own `cause` promotes it to an
|
|
37
|
+
* attribution. Splitting collateral between the files that could have caused it is
|
|
38
|
+
* the one number nothing here measured, and the map counts rather than divides for
|
|
39
|
+
* the same reason the docket does.
|
|
40
|
+
*/
|
|
41
|
+
import type { ReactElement } from 'react';
|
|
42
|
+
import type { BuildDetail, SubjectView } from '../review-types.js';
|
|
43
|
+
/** What became of one render that draws a reached component. */
|
|
44
|
+
export type Landing = 'caused' | 'moved' | 'still' | 'uncompared';
|
|
45
|
+
/** One component the commit arrives at, and every render that draws it. */
|
|
46
|
+
export interface Arrival {
|
|
47
|
+
readonly component: string;
|
|
48
|
+
/** Seed first, component last. Its middle is the chain worth printing. */
|
|
49
|
+
readonly trail: readonly string[];
|
|
50
|
+
/** Set when the chain opens at a file the scan could not read, not at an edit. */
|
|
51
|
+
readonly throughUnread?: string;
|
|
52
|
+
readonly renders: readonly {
|
|
53
|
+
readonly subject: SubjectView;
|
|
54
|
+
readonly landing: Landing;
|
|
55
|
+
}[];
|
|
56
|
+
}
|
|
57
|
+
/** What a changed file turned out to have done, strongest reading first. */
|
|
58
|
+
export type Effect = 'caused' | 'present' | 'still' | 'uncaptured' | 'unreaching';
|
|
59
|
+
export interface Edit {
|
|
60
|
+
readonly file: string;
|
|
61
|
+
readonly arrivals: readonly Arrival[];
|
|
62
|
+
readonly effect: Effect;
|
|
63
|
+
/** Renders this file's components are the named cause of. Distinct subjects. */
|
|
64
|
+
readonly caused: number;
|
|
65
|
+
}
|
|
66
|
+
export interface OutcomeMap {
|
|
67
|
+
readonly edits: readonly Edit[];
|
|
68
|
+
/**
|
|
69
|
+
* Reached components no changed file claims.
|
|
70
|
+
*
|
|
71
|
+
* Their own row, never folded into an edit. A component reached only through a
|
|
72
|
+
* file the scan could not read was reached by the blind spot rather than by the
|
|
73
|
+
* commit, and hanging it under a file somebody actually edited would read as an
|
|
74
|
+
* attribution the traversal declined to make.
|
|
75
|
+
*/
|
|
76
|
+
readonly aside: readonly Arrival[];
|
|
77
|
+
/** Changed subjects the commit reaches none of. Counted here, listed below. */
|
|
78
|
+
readonly unreached: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The map, or `null` when there is no honest one to draw.
|
|
82
|
+
*
|
|
83
|
+
* A run that carried no diff has no left column, and a diff the store could not
|
|
84
|
+
* attribute has a left column that would be a lie — `whole` is the refusal, and
|
|
85
|
+
* drawing files with no edges under them would say the commit reaches nothing.
|
|
86
|
+
* The reach panel prints that refusal in words; this returns nothing rather than
|
|
87
|
+
* printing a second, worse version of it.
|
|
88
|
+
*/
|
|
89
|
+
export declare function outcomeOf(build: BuildDetail): OutcomeMap | null;
|
|
90
|
+
export declare function OutcomeMapView({ build }: {
|
|
91
|
+
readonly build: BuildDetail;
|
|
92
|
+
}): ReactElement | null;
|
|
93
|
+
//# sourceMappingURL=outcome.d.ts.map
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { count, number } from './text.js';
|
|
3
|
+
/**
|
|
4
|
+
* The map, or `null` when there is no honest one to draw.
|
|
5
|
+
*
|
|
6
|
+
* A run that carried no diff has no left column, and a diff the store could not
|
|
7
|
+
* attribute has a left column that would be a lie — `whole` is the refusal, and
|
|
8
|
+
* drawing files with no edges under them would say the commit reaches nothing.
|
|
9
|
+
* The reach panel prints that refusal in words; this returns nothing rather than
|
|
10
|
+
* printing a second, worse version of it.
|
|
11
|
+
*/
|
|
12
|
+
export function outcomeOf(build) {
|
|
13
|
+
const reach = build.reach;
|
|
14
|
+
if (reach === null || reach.whole !== undefined || reach.subjects === undefined)
|
|
15
|
+
return null;
|
|
16
|
+
const drawn = rendersOf(build.subjects, reach);
|
|
17
|
+
const claimed = new Set();
|
|
18
|
+
const edits = reach.changed.map((file) => {
|
|
19
|
+
const arrivals = reach.components
|
|
20
|
+
.filter((component) => component.throughUnread === undefined && component.trail[0] === file)
|
|
21
|
+
.map((component) => ({
|
|
22
|
+
component: component.component,
|
|
23
|
+
trail: component.trail,
|
|
24
|
+
renders: drawn.get(component.component) ?? [],
|
|
25
|
+
}));
|
|
26
|
+
for (const arrival of arrivals)
|
|
27
|
+
claimed.add(arrival.component);
|
|
28
|
+
return { file, arrivals, ...verdictOf(arrivals) };
|
|
29
|
+
});
|
|
30
|
+
const aside = reach.components
|
|
31
|
+
.filter((component) => !claimed.has(component.component))
|
|
32
|
+
.map((component) => ({
|
|
33
|
+
component: component.component,
|
|
34
|
+
trail: component.trail,
|
|
35
|
+
...(component.throughUnread === undefined ? {} : { throughUnread: component.throughUnread }),
|
|
36
|
+
renders: drawn.get(component.component) ?? [],
|
|
37
|
+
}));
|
|
38
|
+
const unreached = build.subjects.filter((subject) => subject.verdict === 'changed' && reach.subjects?.[subject.subject]?.reached === false).length;
|
|
39
|
+
return { edits, aside, unreached };
|
|
40
|
+
}
|
|
41
|
+
/** Every render that draws each component, in the order the report listed them. */
|
|
42
|
+
function rendersOf(subjects, reach) {
|
|
43
|
+
const drawn = new Map();
|
|
44
|
+
for (const subject of subjects) {
|
|
45
|
+
const entry = reach.subjects?.[subject.subject];
|
|
46
|
+
if (entry === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
for (const component of entry.through) {
|
|
49
|
+
const list = drawn.get(component) ?? [];
|
|
50
|
+
list.push({ subject, landing: landingOf(subject, component) });
|
|
51
|
+
drawn.set(component, list);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return drawn;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* What one render says about one component on it.
|
|
58
|
+
*
|
|
59
|
+
* `caused` is the region's own claim and nothing weaker stands in for it. A
|
|
60
|
+
* subject that changed while carrying this component is `moved` — the component
|
|
61
|
+
* was there, something moved, and which of the two facts explains the other is
|
|
62
|
+
* not recorded. Verdicts that describe no comparison stay off the axis entirely:
|
|
63
|
+
* a new subject never had a baseline to hold still against.
|
|
64
|
+
*/
|
|
65
|
+
function landingOf(subject, component) {
|
|
66
|
+
if (subject.verdict === 'unchanged')
|
|
67
|
+
return 'still';
|
|
68
|
+
if (subject.verdict !== 'changed')
|
|
69
|
+
return 'uncompared';
|
|
70
|
+
return subject.regions.some((region) => region.cause === true && region.component === component)
|
|
71
|
+
? 'caused'
|
|
72
|
+
: 'moved';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The strongest true reading of a file, and the count behind it.
|
|
76
|
+
*
|
|
77
|
+
* Strongest rather than most common: a file that caused one difference and
|
|
78
|
+
* reaches four quiet components has caused a difference, and a summary ranked by
|
|
79
|
+
* how many of its arrivals were quiet would file the edit under *nothing moved*.
|
|
80
|
+
*/
|
|
81
|
+
function verdictOf(arrivals) {
|
|
82
|
+
const renders = arrivals.flatMap((arrival) => arrival.renders);
|
|
83
|
+
const caused = new Set(renders.filter((each) => each.landing === 'caused').map((each) => each.subject.subject)).size;
|
|
84
|
+
if (arrivals.length === 0)
|
|
85
|
+
return { effect: 'unreaching', caused: 0 };
|
|
86
|
+
if (caused > 0)
|
|
87
|
+
return { effect: 'caused', caused };
|
|
88
|
+
if (renders.length === 0)
|
|
89
|
+
return { effect: 'uncaptured', caused: 0 };
|
|
90
|
+
if (renders.some((each) => each.landing === 'moved'))
|
|
91
|
+
return { effect: 'present', caused: 0 };
|
|
92
|
+
return { effect: 'still', caused: 0 };
|
|
93
|
+
}
|
|
94
|
+
/** The sentence each effect is worth, and the tone it is worth saying it in. */
|
|
95
|
+
const EFFECTS = {
|
|
96
|
+
caused: { tone: 'va-effect-cause', say: (edit) => `the cause in ${count(edit.caused, 'render')}` },
|
|
97
|
+
present: { tone: 'va-effect-quiet', say: () => 'present, never the cause' },
|
|
98
|
+
still: { tone: 'va-effect-ask', say: () => 'nothing it reaches moved' },
|
|
99
|
+
uncaptured: { tone: 'va-effect-ask', say: () => 'nothing renders what it reaches' },
|
|
100
|
+
unreaching: { tone: 'va-effect-quiet', say: () => 'reaches no component' },
|
|
101
|
+
};
|
|
102
|
+
export function OutcomeMapView({ build }) {
|
|
103
|
+
const map = outcomeOf(build);
|
|
104
|
+
if (map === null)
|
|
105
|
+
return null;
|
|
106
|
+
return (_jsxs("div", { className: "va-map", children: [_jsx("ol", { className: "va-edits", children: map.edits.map((edit) => (_jsx(EditRow, { edit: edit }, edit.file))) }), map.aside.length === 0 ? null : (_jsxs("div", { className: "va-map-aside", children: [_jsxs("p", { className: "va-map-note", children: [count(map.aside.length, 'component'), " reached by a chain that does not open at a changed file. The traversal seeds every file it could not read, so these were reached by the scan\u2019s blind spot rather than by the commit."] }), _jsx("ul", { className: "va-arrivals", children: map.aside.map((arrival) => (_jsx(ArrivalRow, { arrival: arrival }, arrival.component))) })] })), map.unreached === 0 ? null : (_jsxs("p", { className: "va-map-note va-map-alarm", children: [count(map.unreached, 'render'), " moved with no edge on this map \u2014 nothing in the commit arrives there. They are counted below, with their record."] }))] }));
|
|
107
|
+
}
|
|
108
|
+
function EditRow({ edit }) {
|
|
109
|
+
const effect = EFFECTS[edit.effect];
|
|
110
|
+
return (_jsxs("li", { className: "va-edit", children: [_jsxs("p", { className: "va-edit-head", children: [_jsx("code", { className: "va-edit-file", children: edit.file }), _jsx("span", { className: `va-effect ${effect.tone}`, children: effect.say(edit) })] }), edit.arrivals.length === 0 ? null : (_jsx("ul", { className: "va-arrivals", children: edit.arrivals.map((arrival) => (_jsx(ArrivalRow, { arrival: arrival }, arrival.component))) }))] }));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* One component, its chain, and a bar of what became of the renders drawing it.
|
|
114
|
+
*
|
|
115
|
+
* The bar is the reason this is a map rather than a table: eleven renders under
|
|
116
|
+
* one component is a number, and ten orange with one green beside it is the shape
|
|
117
|
+
* of an edit that missed one variant. The counts are printed beside it, because a
|
|
118
|
+
* bar alone is a picture of a proportion nobody can quote.
|
|
119
|
+
*/
|
|
120
|
+
function ArrivalRow({ arrival }) {
|
|
121
|
+
const tally = {
|
|
122
|
+
caused: arrival.renders.filter((each) => each.landing === 'caused').length,
|
|
123
|
+
moved: arrival.renders.filter((each) => each.landing === 'moved').length,
|
|
124
|
+
still: arrival.renders.filter((each) => each.landing === 'still').length,
|
|
125
|
+
uncompared: arrival.renders.filter((each) => each.landing === 'uncompared').length,
|
|
126
|
+
};
|
|
127
|
+
const through = arrival.trail.slice(1, -1);
|
|
128
|
+
return (_jsxs("li", { className: "va-arrival", children: [_jsx("span", { className: "va-arrival-name", children: arrival.component }), through.length === 0 ? null : (_jsxs("span", { className: "va-arrival-through", title: arrival.trail.join(' → '), children: ["through ", through.length === 1 ? through[0] : count(through.length, 'file')] })), arrival.throughUnread === undefined ? null : (_jsx("span", { className: "va-arrival-through", title: arrival.throughUnread, children: "from an unread file" })), _jsx(Bar, { tally: tally }), _jsx("span", { className: "va-arrival-tally va-num", children: phrase(tally) })] }));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The proportion, with no bar drawn for a component nothing renders.
|
|
132
|
+
*
|
|
133
|
+
* An empty track and a track of one colour are different findings and would look
|
|
134
|
+
* identical at this size, so the empty one is not drawn at all — the phrase beside
|
|
135
|
+
* it is the whole answer there.
|
|
136
|
+
*/
|
|
137
|
+
function Bar({ tally }) {
|
|
138
|
+
const total = tally.caused + tally.moved + tally.still + tally.uncompared;
|
|
139
|
+
if (total === 0)
|
|
140
|
+
return null;
|
|
141
|
+
return (_jsx("span", { className: "va-bar", "aria-hidden": "true", children: ['caused', 'moved', 'still', 'uncompared'].map((landing) => tally[landing] === 0 ? null : (_jsx("span", { className: `va-bar-part va-bar-${landing}`, style: { flexGrow: tally[landing] } }, landing))) }));
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The counts, said as a reviewer would say them.
|
|
145
|
+
*
|
|
146
|
+
* `still` is printed even when it is the only thing to print, because *four
|
|
147
|
+
* renders, none of them moved* is the finding on this row. A phrase that listed
|
|
148
|
+
* only what moved would render that row blank.
|
|
149
|
+
*/
|
|
150
|
+
function phrase(tally) {
|
|
151
|
+
const total = tally.caused + tally.moved + tally.still + tally.uncompared;
|
|
152
|
+
if (total === 0)
|
|
153
|
+
return 'no render draws it';
|
|
154
|
+
const parts = [
|
|
155
|
+
tally.caused === 0 ? undefined : `${number(tally.caused)} caused`,
|
|
156
|
+
tally.moved === 0 ? undefined : `${number(tally.moved)} moved`,
|
|
157
|
+
tally.still === 0 ? undefined : `${number(tally.still)} still`,
|
|
158
|
+
tally.uncompared === 0 ? undefined : `${number(tally.uncompared)} not compared`,
|
|
159
|
+
].filter((part) => part !== undefined);
|
|
160
|
+
return `${count(total, 'render')} · ${parts.join(' · ')}`;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=outcome.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outcome.js","sourceRoot":"","sources":["../../src/ui/outcome.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edit, drawn as far as it got.\n *\n * Every other panel starts from a difference and works backwards. This one starts\n * from the commit and works forwards: the files somebody changed, the components\n * the import graph carries them to, and what happened on the renders those\n * components appear in. Three records that already exist, joined on the one axis\n * none of them carries alone — the diff knows nothing about renders, the graph\n * knows nothing about pixels, and the comparison knows nothing about what was\n * edited.\n *\n * ## Five things a file can turn out to have done\n *\n * Only the first is the one everybody builds for.\n *\n * - **the cause in N renders** — a component reached from here is named as the\n * cause of a difference. The ordinary case.\n * - **present, never the cause** — it renders on screens that moved, and no region\n * in any of them names it. The screens changed for somebody else's reasons.\n * - **nothing it reaches moved** — every render carrying it held still. An author\n * who believed they were changing this surface finds out here, and nowhere else\n * in this category: a comparison tool is silent about a green subject, and\n * silence is what *worked* looks like too.\n * - **nothing renders what it reaches** — the components exist, the graph arrives\n * at them, and no subject in the suite draws one. That is a coverage hole\n * reported against the edit that just walked into it, rather than against a\n * percentage nobody reads.\n * - **reaches no component** — a server file, a config, a test. Named rather than\n * hidden, because a file the graph drops is the other reason a change can be\n * invisible, and it must not look like the case above it.\n *\n * ## What is not claimed\n *\n * A render moving is not evidence that a file on it moved it. `present, never the\n * cause` is exactly that refusal wearing a label: the edge from file to render is\n * the import graph's, and only the region's own `cause` promotes it to an\n * attribution. Splitting collateral between the files that could have caused it is\n * the one number nothing here measured, and the map counts rather than divides for\n * the same reason the docket does.\n */\n\nimport type { ReactElement } from 'react';\nimport type { BuildDetail, ReachView, SubjectView } from '../review-types.js';\nimport { count, number } from './text.js';\n\n/** What became of one render that draws a reached component. */\nexport type Landing = 'caused' | 'moved' | 'still' | 'uncompared';\n\n/** One component the commit arrives at, and every render that draws it. */\nexport interface Arrival {\n readonly component: string;\n /** Seed first, component last. Its middle is the chain worth printing. */\n readonly trail: readonly string[];\n /** Set when the chain opens at a file the scan could not read, not at an edit. */\n readonly throughUnread?: string;\n readonly renders: readonly { readonly subject: SubjectView; readonly landing: Landing }[];\n}\n\n/** What a changed file turned out to have done, strongest reading first. */\nexport type Effect = 'caused' | 'present' | 'still' | 'uncaptured' | 'unreaching';\n\nexport interface Edit {\n readonly file: string;\n readonly arrivals: readonly Arrival[];\n readonly effect: Effect;\n /** Renders this file's components are the named cause of. Distinct subjects. */\n readonly caused: number;\n}\n\nexport interface OutcomeMap {\n readonly edits: readonly Edit[];\n /**\n * Reached components no changed file claims.\n *\n * Their own row, never folded into an edit. A component reached only through a\n * file the scan could not read was reached by the blind spot rather than by the\n * commit, and hanging it under a file somebody actually edited would read as an\n * attribution the traversal declined to make.\n */\n readonly aside: readonly Arrival[];\n /** Changed subjects the commit reaches none of. Counted here, listed below. */\n readonly unreached: number;\n}\n\n/**\n * The map, or `null` when there is no honest one to draw.\n *\n * A run that carried no diff has no left column, and a diff the store could not\n * attribute has a left column that would be a lie — `whole` is the refusal, and\n * drawing files with no edges under them would say the commit reaches nothing.\n * The reach panel prints that refusal in words; this returns nothing rather than\n * printing a second, worse version of it.\n */\nexport function outcomeOf(build: BuildDetail): OutcomeMap | null {\n const reach = build.reach;\n if (reach === null || reach.whole !== undefined || reach.subjects === undefined) return null;\n\n const drawn = rendersOf(build.subjects, reach);\n const claimed = new Set<string>();\n const edits = reach.changed.map((file) => {\n const arrivals = reach.components\n .filter((component) => component.throughUnread === undefined && component.trail[0] === file)\n .map((component): Arrival => ({\n component: component.component,\n trail: component.trail,\n renders: drawn.get(component.component) ?? [],\n }));\n for (const arrival of arrivals) claimed.add(arrival.component);\n return { file, arrivals, ...verdictOf(arrivals) };\n });\n\n const aside = reach.components\n .filter((component) => !claimed.has(component.component))\n .map((component): Arrival => ({\n component: component.component,\n trail: component.trail,\n ...(component.throughUnread === undefined ? {} : { throughUnread: component.throughUnread }),\n renders: drawn.get(component.component) ?? [],\n }));\n\n const unreached = build.subjects.filter(\n (subject) => subject.verdict === 'changed' && reach.subjects?.[subject.subject]?.reached === false,\n ).length;\n\n return { edits, aside, unreached };\n}\n\n/** Every render that draws each component, in the order the report listed them. */\nfunction rendersOf(\n subjects: readonly SubjectView[],\n reach: ReachView,\n): Map<string, { readonly subject: SubjectView; readonly landing: Landing }[]> {\n const drawn = new Map<string, { readonly subject: SubjectView; readonly landing: Landing }[]>();\n for (const subject of subjects) {\n const entry = reach.subjects?.[subject.subject];\n if (entry === undefined) continue;\n for (const component of entry.through) {\n const list = drawn.get(component) ?? [];\n list.push({ subject, landing: landingOf(subject, component) });\n drawn.set(component, list);\n }\n }\n return drawn;\n}\n\n/**\n * What one render says about one component on it.\n *\n * `caused` is the region's own claim and nothing weaker stands in for it. A\n * subject that changed while carrying this component is `moved` — the component\n * was there, something moved, and which of the two facts explains the other is\n * not recorded. Verdicts that describe no comparison stay off the axis entirely:\n * a new subject never had a baseline to hold still against.\n */\nfunction landingOf(subject: SubjectView, component: string): Landing {\n if (subject.verdict === 'unchanged') return 'still';\n if (subject.verdict !== 'changed') return 'uncompared';\n return subject.regions.some((region) => region.cause === true && region.component === component)\n ? 'caused'\n : 'moved';\n}\n\n/**\n * The strongest true reading of a file, and the count behind it.\n *\n * Strongest rather than most common: a file that caused one difference and\n * reaches four quiet components has caused a difference, and a summary ranked by\n * how many of its arrivals were quiet would file the edit under *nothing moved*.\n */\nfunction verdictOf(arrivals: readonly Arrival[]): { readonly effect: Effect; readonly caused: number } {\n const renders = arrivals.flatMap((arrival) => arrival.renders);\n const caused = new Set(\n renders.filter((each) => each.landing === 'caused').map((each) => each.subject.subject),\n ).size;\n\n if (arrivals.length === 0) return { effect: 'unreaching', caused: 0 };\n if (caused > 0) return { effect: 'caused', caused };\n if (renders.length === 0) return { effect: 'uncaptured', caused: 0 };\n if (renders.some((each) => each.landing === 'moved')) return { effect: 'present', caused: 0 };\n return { effect: 'still', caused: 0 };\n}\n\n/** The sentence each effect is worth, and the tone it is worth saying it in. */\nconst EFFECTS: Readonly<Record<Effect, { readonly tone: string; readonly say: (edit: Edit) => string }>> = {\n caused: { tone: 'va-effect-cause', say: (edit) => `the cause in ${count(edit.caused, 'render')}` },\n present: { tone: 'va-effect-quiet', say: () => 'present, never the cause' },\n still: { tone: 'va-effect-ask', say: () => 'nothing it reaches moved' },\n uncaptured: { tone: 'va-effect-ask', say: () => 'nothing renders what it reaches' },\n unreaching: { tone: 'va-effect-quiet', say: () => 'reaches no component' },\n};\n\nexport function OutcomeMapView({ build }: { readonly build: BuildDetail }): ReactElement | null {\n const map = outcomeOf(build);\n if (map === null) return null;\n\n return (\n <div className=\"va-map\">\n <ol className=\"va-edits\">\n {map.edits.map((edit) => (\n <EditRow key={edit.file} edit={edit} />\n ))}\n </ol>\n\n {map.aside.length === 0 ? null : (\n <div className=\"va-map-aside\">\n <p className=\"va-map-note\">\n {count(map.aside.length, 'component')} reached by a chain that does not open at a\n changed file. The traversal seeds every file it could not read, so these were reached by\n the scan’s blind spot rather than by the commit.\n </p>\n <ul className=\"va-arrivals\">\n {map.aside.map((arrival) => (\n <ArrivalRow key={arrival.component} arrival={arrival} />\n ))}\n </ul>\n </div>\n )}\n\n {map.unreached === 0 ? null : (\n <p className=\"va-map-note va-map-alarm\">\n {count(map.unreached, 'render')} moved with no edge on this map — nothing in the commit\n arrives there. They are counted below, with their record.\n </p>\n )}\n </div>\n );\n}\n\nfunction EditRow({ edit }: { readonly edit: Edit }): ReactElement {\n const effect = EFFECTS[edit.effect];\n\n return (\n <li className=\"va-edit\">\n <p className=\"va-edit-head\">\n <code className=\"va-edit-file\">{edit.file}</code>\n <span className={`va-effect ${effect.tone}`}>{effect.say(edit)}</span>\n </p>\n {edit.arrivals.length === 0 ? null : (\n <ul className=\"va-arrivals\">\n {edit.arrivals.map((arrival) => (\n <ArrivalRow key={arrival.component} arrival={arrival} />\n ))}\n </ul>\n )}\n </li>\n );\n}\n\n/**\n * One component, its chain, and a bar of what became of the renders drawing it.\n *\n * The bar is the reason this is a map rather than a table: eleven renders under\n * one component is a number, and ten orange with one green beside it is the shape\n * of an edit that missed one variant. The counts are printed beside it, because a\n * bar alone is a picture of a proportion nobody can quote.\n */\nfunction ArrivalRow({ arrival }: { readonly arrival: Arrival }): ReactElement {\n const tally = {\n caused: arrival.renders.filter((each) => each.landing === 'caused').length,\n moved: arrival.renders.filter((each) => each.landing === 'moved').length,\n still: arrival.renders.filter((each) => each.landing === 'still').length,\n uncompared: arrival.renders.filter((each) => each.landing === 'uncompared').length,\n };\n const through = arrival.trail.slice(1, -1);\n\n return (\n <li className=\"va-arrival\">\n <span className=\"va-arrival-name\">{arrival.component}</span>\n {through.length === 0 ? null : (\n <span className=\"va-arrival-through\" title={arrival.trail.join(' → ')}>\n through {through.length === 1 ? through[0] : count(through.length, 'file')}\n </span>\n )}\n {arrival.throughUnread === undefined ? null : (\n <span className=\"va-arrival-through\" title={arrival.throughUnread}>\n from an unread file\n </span>\n )}\n <Bar tally={tally} />\n <span className=\"va-arrival-tally va-num\">{phrase(tally)}</span>\n </li>\n );\n}\n\ninterface Tally {\n readonly caused: number;\n readonly moved: number;\n readonly still: number;\n readonly uncompared: number;\n}\n\n/**\n * The proportion, with no bar drawn for a component nothing renders.\n *\n * An empty track and a track of one colour are different findings and would look\n * identical at this size, so the empty one is not drawn at all — the phrase beside\n * it is the whole answer there.\n */\nfunction Bar({ tally }: { readonly tally: Tally }): ReactElement | null {\n const total = tally.caused + tally.moved + tally.still + tally.uncompared;\n if (total === 0) return null;\n\n return (\n <span className=\"va-bar\" aria-hidden=\"true\">\n {(['caused', 'moved', 'still', 'uncompared'] as const).map((landing) =>\n tally[landing] === 0 ? null : (\n <span\n key={landing}\n className={`va-bar-part va-bar-${landing}`}\n style={{ flexGrow: tally[landing] }}\n />\n ),\n )}\n </span>\n );\n}\n\n/**\n * The counts, said as a reviewer would say them.\n *\n * `still` is printed even when it is the only thing to print, because *four\n * renders, none of them moved* is the finding on this row. A phrase that listed\n * only what moved would render that row blank.\n */\nfunction phrase(tally: Tally): string {\n const total = tally.caused + tally.moved + tally.still + tally.uncompared;\n if (total === 0) return 'no render draws it';\n\n const parts = [\n tally.caused === 0 ? undefined : `${number(tally.caused)} caused`,\n tally.moved === 0 ? undefined : `${number(tally.moved)} moved`,\n tally.still === 0 ? undefined : `${number(tally.still)} still`,\n tally.uncompared === 0 ? undefined : `${number(tally.uncompared)} not compared`,\n ].filter((part): part is string => part !== undefined);\n\n return `${count(total, 'render')} · ${parts.join(' · ')}`;\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where one change's renders stopped agreeing with each other.
|
|
3
|
+
*
|
|
4
|
+
* A change is approved once and lands many times. `Button` moved in seven
|
|
5
|
+
* renders and the page says so, which reads as one thing having happened seven
|
|
6
|
+
* times — and on this build it is two things. Two of those renders moved by
|
|
7
|
+
* 6,418 pixels into one shape and five moved by about 3,500 into another, so a
|
|
8
|
+
* reviewer pressing approve is signing off on two outcomes under one name and
|
|
9
|
+
* has been shown neither.
|
|
10
|
+
*
|
|
11
|
+
* The shape is already recorded: a region carries a fingerprint of the
|
|
12
|
+
* difference, so renders that moved *identically* share one and renders that
|
|
13
|
+
* absorbed the same edit differently do not. Grouping by it costs nothing and
|
|
14
|
+
* turns `7 renders · 2 shapes` — a count nobody can act on — into the two groups
|
|
15
|
+
* and what separates them.
|
|
16
|
+
*
|
|
17
|
+
* ## Not `partingOf`
|
|
18
|
+
*
|
|
19
|
+
* `@variance-authority/core`'s parting reads two documents and climbs to the
|
|
20
|
+
* hook cell that decided a difference. This never opens a document. It divides
|
|
21
|
+
* one component's renders by the digest of what happened to each, which is the
|
|
22
|
+
* same question — *where did these part* — asked at the only grain a review
|
|
23
|
+
* surface has, and answerable on data the build already carried.
|
|
24
|
+
*
|
|
25
|
+
* ## The shared name is a name, not a cause
|
|
26
|
+
*
|
|
27
|
+
* When every subject in a group extends one prefix and no other group's does,
|
|
28
|
+
* that prefix is printed. It is the inference `VariationRecord`'s `named`
|
|
29
|
+
* already makes — a subject whose name extends another's is that subject plus an
|
|
30
|
+
* axis — and it is correct exactly as often as the naming convention is kept. So
|
|
31
|
+
* it is printed as a label beside the group and never as a because: *these two
|
|
32
|
+
* are the `route/detail` renders*, not *`route/detail` caused this*.
|
|
33
|
+
*
|
|
34
|
+
* One shape is reported too. *Seven renders, one shape* is the answer that makes
|
|
35
|
+
* the divided case legible, and a section that appeared only on divergence would
|
|
36
|
+
* leave a reviewer unable to tell agreement from a panel that had not loaded.
|
|
37
|
+
*/
|
|
38
|
+
import type { Origin } from './grouping.js';
|
|
39
|
+
/** One group of renders that moved identically. */
|
|
40
|
+
export interface Shape {
|
|
41
|
+
/** The fingerprint they share. Not printed; it is an identity, not a finding. */
|
|
42
|
+
readonly shape: string;
|
|
43
|
+
/** Subjects in it, sorted. */
|
|
44
|
+
readonly renders: readonly string[];
|
|
45
|
+
readonly least: number;
|
|
46
|
+
readonly most: number;
|
|
47
|
+
/**
|
|
48
|
+
* The name every render here extends and no other group's does.
|
|
49
|
+
*
|
|
50
|
+
* Absent when the group has one render — the subject is already the coordinate
|
|
51
|
+
* — or when the names have nothing in common that is theirs alone.
|
|
52
|
+
*/
|
|
53
|
+
readonly only?: string;
|
|
54
|
+
}
|
|
55
|
+
/** One change's renders, divided by what happened to each. */
|
|
56
|
+
export interface Parted {
|
|
57
|
+
readonly shapes: readonly Shape[];
|
|
58
|
+
/**
|
|
59
|
+
* Renders whose leading region recorded no fingerprint.
|
|
60
|
+
*
|
|
61
|
+
* Their own list, never a shape. Two renders that both recorded nothing have
|
|
62
|
+
* not been found to agree, and folding them together would report the
|
|
63
|
+
* identical difference twice about a pair nothing measured.
|
|
64
|
+
*/
|
|
65
|
+
readonly unshaped: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
export declare function partedBy(origin: Origin): Parted;
|
|
68
|
+
//# sourceMappingURL=parted.d.ts.map
|