@variance-authority/tribunal 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/dist/__fixtures__/review.d.ts +47 -0
- package/dist/__fixtures__/review.js +145 -0
- package/dist/__fixtures__/review.js.map +1 -0
- package/dist/bindings.d.ts +134 -0
- package/dist/bindings.js +88 -0
- package/dist/bindings.js.map +1 -0
- package/dist/changelog.d.ts +132 -0
- package/dist/changelog.js +129 -0
- package/dist/changelog.js.map +1 -0
- package/dist/history-queries.d.ts +82 -0
- package/dist/history-queries.js +200 -0
- package/dist/history-queries.js.map +1 -0
- package/dist/history-rows.d.ts +42 -0
- package/dist/history-rows.js +148 -0
- package/dist/history-rows.js.map +1 -0
- package/dist/history.d.ts +40 -0
- package/dist/history.js +186 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/migration-steps.d.ts +32 -0
- package/dist/migration-steps.js +303 -0
- package/dist/migration-steps.js.map +1 -0
- package/dist/migrations.d.ts +58 -0
- package/dist/migrations.js +283 -0
- package/dist/migrations.js.map +1 -0
- package/dist/next.d.ts +94 -0
- package/dist/next.js +44 -0
- package/dist/next.js.map +1 -0
- package/dist/node/bin.d.ts +120 -0
- package/dist/node/bin.js +337 -0
- package/dist/node/bin.js.map +1 -0
- package/dist/node/bucket.d.ts +58 -0
- package/dist/node/bucket.js +142 -0
- package/dist/node/bucket.js.map +1 -0
- package/dist/node/database.d.ts +33 -0
- package/dist/node/database.js +157 -0
- package/dist/node/database.js.map +1 -0
- package/dist/node/index.d.ts +43 -0
- package/dist/node/index.js +43 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/serve.d.ts +81 -0
- package/dist/node/serve.js +193 -0
- package/dist/node/serve.js.map +1 -0
- package/dist/node/ui-assets.d.ts +47 -0
- package/dist/node/ui-assets.js +60 -0
- package/dist/node/ui-assets.js.map +1 -0
- package/dist/review-ingest.d.ts +24 -0
- package/dist/review-ingest.js +184 -0
- package/dist/review-ingest.js.map +1 -0
- package/dist/review-read.d.ts +87 -0
- package/dist/review-read.js +361 -0
- package/dist/review-read.js.map +1 -0
- package/dist/review-rows.d.ts +30 -0
- package/dist/review-rows.js +67 -0
- package/dist/review-rows.js.map +1 -0
- package/dist/review-types.d.ts +414 -0
- package/dist/review-types.js +2 -0
- package/dist/review-types.js.map +1 -0
- package/dist/review-write.d.ts +35 -0
- package/dist/review-write.js +86 -0
- package/dist/review-write.js.map +1 -0
- package/dist/review.d.ts +53 -0
- package/dist/review.fixtures.d.ts +47 -0
- package/dist/review.fixtures.js +145 -0
- package/dist/review.fixtures.js.map +1 -0
- package/dist/review.js +245 -0
- package/dist/review.js.map +1 -0
- package/dist/schema.d.ts +36 -0
- package/dist/schema.js +39 -0
- package/dist/schema.js.map +1 -0
- package/dist/store.d.ts +58 -0
- package/dist/store.js +278 -0
- package/dist/store.js.map +1 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +92 -0
- package/dist/testing.js.map +1 -0
- package/dist/ui/__probe.d.ts +3 -0
- package/dist/ui/__probe.js +5 -0
- package/dist/ui/__probe.js.map +1 -0
- package/dist/ui/attribution.d.ts +46 -0
- package/dist/ui/attribution.js +79 -0
- package/dist/ui/attribution.js.map +1 -0
- package/dist/ui/because.d.ts +51 -0
- package/dist/ui/because.js +52 -0
- package/dist/ui/because.js.map +1 -0
- package/dist/ui/browser-entry.d.ts +2 -0
- package/dist/ui/browser-entry.js +34 -0
- package/dist/ui/browser-entry.js.map +1 -0
- package/dist/ui/builds.d.ts +43 -0
- package/dist/ui/builds.js +79 -0
- package/dist/ui/builds.js.map +1 -0
- package/dist/ui/carried.d.ts +73 -0
- package/dist/ui/carried.js +63 -0
- package/dist/ui/carried.js.map +1 -0
- package/dist/ui/cause.d.ts +73 -0
- package/dist/ui/cause.js +133 -0
- package/dist/ui/cause.js.map +1 -0
- package/dist/ui/change-story.d.ts +153 -0
- package/dist/ui/change-story.js +253 -0
- package/dist/ui/change-story.js.map +1 -0
- package/dist/ui/change.d.ts +51 -0
- package/dist/ui/change.js +125 -0
- package/dist/ui/change.js.map +1 -0
- package/dist/ui/client.d.ts +74 -0
- package/dist/ui/client.js +88 -0
- package/dist/ui/client.js.map +1 -0
- package/dist/ui/consumers.d.ts +80 -0
- package/dist/ui/consumers.js +125 -0
- package/dist/ui/consumers.js.map +1 -0
- package/dist/ui/control.d.ts +63 -0
- package/dist/ui/control.js +39 -0
- package/dist/ui/control.js.map +1 -0
- package/dist/ui/crossing.d.ts +55 -0
- package/dist/ui/crossing.js +74 -0
- package/dist/ui/crossing.js.map +1 -0
- package/dist/ui/declarations.d.ts +36 -0
- package/dist/ui/declarations.js +73 -0
- package/dist/ui/declarations.js.map +1 -0
- package/dist/ui/distance.d.ts +154 -0
- package/dist/ui/distance.js +180 -0
- package/dist/ui/distance.js.map +1 -0
- package/dist/ui/divergence.d.ts +40 -0
- package/dist/ui/divergence.js +176 -0
- package/dist/ui/divergence.js.map +1 -0
- package/dist/ui/docket.d.ts +39 -0
- package/dist/ui/docket.js +172 -0
- package/dist/ui/docket.js.map +1 -0
- package/dist/ui/findings.d.ts +42 -0
- package/dist/ui/findings.js +110 -0
- package/dist/ui/findings.js.map +1 -0
- package/dist/ui/foreseen.d.ts +55 -0
- package/dist/ui/foreseen.js +91 -0
- package/dist/ui/foreseen.js.map +1 -0
- package/dist/ui/glance.d.ts +101 -0
- package/dist/ui/glance.js +101 -0
- package/dist/ui/glance.js.map +1 -0
- package/dist/ui/grouping.d.ts +158 -0
- package/dist/ui/grouping.js +187 -0
- package/dist/ui/grouping.js.map +1 -0
- package/dist/ui/handed.d.ts +60 -0
- package/dist/ui/handed.js +122 -0
- package/dist/ui/handed.js.map +1 -0
- package/dist/ui/history.d.ts +88 -0
- package/dist/ui/history.js +170 -0
- package/dist/ui/history.js.map +1 -0
- package/dist/ui/holding.d.ts +84 -0
- package/dist/ui/holding.js +90 -0
- package/dist/ui/holding.js.map +1 -0
- package/dist/ui/impact.d.ts +31 -0
- package/dist/ui/impact.js +132 -0
- package/dist/ui/impact.js.map +1 -0
- package/dist/ui/index.d.ts +21 -0
- package/dist/ui/index.js +21 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/lead.d.ts +35 -0
- package/dist/ui/lead.js +34 -0
- package/dist/ui/lead.js.map +1 -0
- package/dist/ui/look.d.ts +109 -0
- package/dist/ui/look.js +248 -0
- package/dist/ui/look.js.map +1 -0
- package/dist/ui/mark.d.ts +11 -0
- package/dist/ui/mark.js +13 -0
- package/dist/ui/mark.js.map +1 -0
- package/dist/ui/moved.d.ts +121 -0
- package/dist/ui/moved.js +143 -0
- package/dist/ui/moved.js.map +1 -0
- package/dist/ui/navigation.d.ts +26 -0
- package/dist/ui/navigation.js +53 -0
- package/dist/ui/navigation.js.map +1 -0
- package/dist/ui/order.d.ts +113 -0
- package/dist/ui/order.js +226 -0
- package/dist/ui/order.js.map +1 -0
- package/dist/ui/origins.d.ts +42 -0
- package/dist/ui/origins.js +149 -0
- package/dist/ui/origins.js.map +1 -0
- package/dist/ui/outcome.d.ts +93 -0
- package/dist/ui/outcome.js +162 -0
- package/dist/ui/outcome.js.map +1 -0
- package/dist/ui/parted.d.ts +68 -0
- package/dist/ui/parted.js +101 -0
- package/dist/ui/parted.js.map +1 -0
- package/dist/ui/rail.d.ts +31 -0
- package/dist/ui/rail.js +21 -0
- package/dist/ui/rail.js.map +1 -0
- package/dist/ui/reach.d.ts +66 -0
- package/dist/ui/reach.js +185 -0
- package/dist/ui/reach.js.map +1 -0
- package/dist/ui/regions.d.ts +82 -0
- package/dist/ui/regions.js +67 -0
- package/dist/ui/regions.js.map +1 -0
- package/dist/ui/review.bundle.js +69 -0
- package/dist/ui/review.d.ts +97 -0
- package/dist/ui/review.js +39 -0
- package/dist/ui/review.js.map +1 -0
- package/dist/ui/root.d.ts +72 -0
- package/dist/ui/root.js +102 -0
- package/dist/ui/root.js.map +1 -0
- package/dist/ui/route.d.ts +95 -0
- package/dist/ui/route.js +114 -0
- package/dist/ui/route.js.map +1 -0
- package/dist/ui/run.d.ts +26 -0
- package/dist/ui/run.js +12 -0
- package/dist/ui/run.js.map +1 -0
- package/dist/ui/sense.d.ts +187 -0
- package/dist/ui/sense.js +222 -0
- package/dist/ui/sense.js.map +1 -0
- package/dist/ui/settled.d.ts +43 -0
- package/dist/ui/settled.js +59 -0
- package/dist/ui/settled.js.map +1 -0
- package/dist/ui/shell.d.ts +76 -0
- package/dist/ui/shell.js +65 -0
- package/dist/ui/shell.js.map +1 -0
- package/dist/ui/shift.d.ts +52 -0
- package/dist/ui/shift.js +98 -0
- package/dist/ui/shift.js.map +1 -0
- package/dist/ui/styles-docket.d.ts +18 -0
- package/dist/ui/styles-docket.js +281 -0
- package/dist/ui/styles-docket.js.map +1 -0
- package/dist/ui/styles-stage.d.ts +18 -0
- package/dist/ui/styles-stage.js +92 -0
- package/dist/ui/styles-stage.js.map +1 -0
- package/dist/ui/styles.d.ts +12 -0
- package/dist/ui/styles.js +482 -0
- package/dist/ui/styles.js.map +1 -0
- package/dist/ui/subject.d.ts +36 -0
- package/dist/ui/subject.js +138 -0
- package/dist/ui/subject.js.map +1 -0
- package/dist/ui/text.d.ts +129 -0
- package/dist/ui/text.js +164 -0
- package/dist/ui/text.js.map +1 -0
- package/dist/ui/variations.d.ts +37 -0
- package/dist/ui/variations.js +60 -0
- package/dist/ui/variations.js.map +1 -0
- package/dist/ui/viewer.d.ts +67 -0
- package/dist/ui/viewer.js +246 -0
- package/dist/ui/viewer.js.map +1 -0
- package/dist/worker-auth.d.ts +48 -0
- package/dist/worker-auth.js +74 -0
- package/dist/worker-auth.js.map +1 -0
- package/dist/worker-entry.d.ts +58 -0
- package/dist/worker-entry.js +36 -0
- package/dist/worker-entry.js.map +1 -0
- package/dist/worker-http.d.ts +39 -0
- package/dist/worker-http.js +111 -0
- package/dist/worker-http.js.map +1 -0
- package/dist/worker-input.d.ts +81 -0
- package/dist/worker-input.js +296 -0
- package/dist/worker-input.js.map +1 -0
- package/dist/worker.d.ts +91 -0
- package/dist/worker.js +287 -0
- package/dist/worker.js.map +1 -0
- package/mark.svg +30 -0
- package/migrations/0001_initial.sql +193 -0
- package/migrations/0002_changelog.sql +30 -0
- package/migrations/0003_accessibility.sql +8 -0
- package/migrations/0004_variations.sql +19 -0
- package/migrations/0005_reach.sql +26 -0
- package/migrations/0006_baseline-size.sql +7 -0
- package/migrations/0007_declarations.sql +7 -0
- package/migrations/0008_green-because.sql +7 -0
- package/migrations/0009_moved-bands.sql +6 -0
- package/migrations/0010_composition.sql +15 -0
- package/migrations/0011_movements.sql +21 -0
- package/migrations/0012_control-size.sql +6 -0
- package/package.json +110 -0
- package/wrangler.jsonc +45 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 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
|
+
export function partedBy(origin) {
|
|
39
|
+
const groups = new Map();
|
|
40
|
+
const unshaped = [];
|
|
41
|
+
for (const each of origin.appearances) {
|
|
42
|
+
if (each.shape === undefined) {
|
|
43
|
+
unshaped.push(each.subject.subject);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
groups.set(each.shape, [...(groups.get(each.shape) ?? []), each]);
|
|
47
|
+
}
|
|
48
|
+
const found = [...groups].map(([shape, members]) => {
|
|
49
|
+
const pixels = members.map((each) => each.pixels);
|
|
50
|
+
return {
|
|
51
|
+
shape,
|
|
52
|
+
renders: members.map((each) => each.subject.subject).sort(),
|
|
53
|
+
least: Math.min(...pixels),
|
|
54
|
+
most: Math.max(...pixels),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
const shapes = found
|
|
58
|
+
.map((group) => {
|
|
59
|
+
const only = nameOf(group.renders, found.filter((other) => other.shape !== group.shape).flatMap((other) => other.renders));
|
|
60
|
+
return only === undefined ? group : { ...group, only };
|
|
61
|
+
})
|
|
62
|
+
.sort((left, right) => right.most - left.most || left.shape.localeCompare(right.shape));
|
|
63
|
+
return { shapes, unshaped: unshaped.sort() };
|
|
64
|
+
}
|
|
65
|
+
/** Boundaries a subject name is built from, so a prefix is cut at a joint. */
|
|
66
|
+
const JOINTS = new Set(['/', '@', ':', '-', '.', '_']);
|
|
67
|
+
/**
|
|
68
|
+
* The name these renders share, when no other group's renders share it.
|
|
69
|
+
*
|
|
70
|
+
* `undefined` for a single render, where the prefix would be the subject itself
|
|
71
|
+
* and printing it beside the subject says nothing twice.
|
|
72
|
+
*
|
|
73
|
+
* The common prefix is only a name when it ends where the renders do — at a
|
|
74
|
+
* joint, or at the end of one of them. `route/detail@1280` and
|
|
75
|
+
* `route/detail@1024` have `route/detail@1` in common and no such viewport
|
|
76
|
+
* exists; the name is `route/detail`, which is then shared with the other group
|
|
77
|
+
* and so is not printed at all.
|
|
78
|
+
*/
|
|
79
|
+
function nameOf(renders, others) {
|
|
80
|
+
if (renders.length < 2)
|
|
81
|
+
return undefined;
|
|
82
|
+
const [first = '', ...rest] = renders;
|
|
83
|
+
let cut = first.length;
|
|
84
|
+
for (const each of rest) {
|
|
85
|
+
let index = 0;
|
|
86
|
+
while (index < cut && index < each.length && each[index] === first[index])
|
|
87
|
+
index += 1;
|
|
88
|
+
cut = index;
|
|
89
|
+
}
|
|
90
|
+
if (!renders.every((each) => each.length === cut || JOINTS.has(each[cut] ?? ''))) {
|
|
91
|
+
while (cut > 0 && !JOINTS.has(first[cut - 1] ?? ''))
|
|
92
|
+
cut -= 1;
|
|
93
|
+
}
|
|
94
|
+
while (cut > 0 && JOINTS.has(first[cut - 1] ?? ''))
|
|
95
|
+
cut -= 1;
|
|
96
|
+
const shared = first.slice(0, cut);
|
|
97
|
+
if (shared === '')
|
|
98
|
+
return undefined;
|
|
99
|
+
return others.some((each) => each.startsWith(shared)) ? undefined : shared;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=parted.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parted.js","sourceRoot":"","sources":["../../src/ui/parted.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where one change's renders stopped agreeing with each other.\n *\n * A change is approved once and lands many times. `Button` moved in seven\n * renders and the page says so, which reads as one thing having happened seven\n * times — and on this build it is two things. Two of those renders moved by\n * 6,418 pixels into one shape and five moved by about 3,500 into another, so a\n * reviewer pressing approve is signing off on two outcomes under one name and\n * has been shown neither.\n *\n * The shape is already recorded: a region carries a fingerprint of the\n * difference, so renders that moved *identically* share one and renders that\n * absorbed the same edit differently do not. Grouping by it costs nothing and\n * turns `7 renders · 2 shapes` — a count nobody can act on — into the two groups\n * and what separates them.\n *\n * ## Not `partingOf`\n *\n * `@variance-authority/core`'s parting reads two documents and climbs to the\n * hook cell that decided a difference. This never opens a document. It divides\n * one component's renders by the digest of what happened to each, which is the\n * same question — *where did these part* — asked at the only grain a review\n * surface has, and answerable on data the build already carried.\n *\n * ## The shared name is a name, not a cause\n *\n * When every subject in a group extends one prefix and no other group's does,\n * that prefix is printed. It is the inference `VariationRecord`'s `named`\n * already makes — a subject whose name extends another's is that subject plus an\n * axis — and it is correct exactly as often as the naming convention is kept. So\n * it is printed as a label beside the group and never as a because: *these two\n * are the `route/detail` renders*, not *`route/detail` caused this*.\n *\n * One shape is reported too. *Seven renders, one shape* is the answer that makes\n * the divided case legible, and a section that appeared only on divergence would\n * leave a reviewer unable to tell agreement from a panel that had not loaded.\n */\n\nimport type { Appearance, Origin } from './grouping.js';\n\n/** One group of renders that moved identically. */\nexport interface Shape {\n /** The fingerprint they share. Not printed; it is an identity, not a finding. */\n readonly shape: string;\n /** Subjects in it, sorted. */\n readonly renders: readonly string[];\n readonly least: number;\n readonly most: number;\n /**\n * The name every render here extends and no other group's does.\n *\n * Absent when the group has one render — the subject is already the coordinate\n * — or when the names have nothing in common that is theirs alone.\n */\n readonly only?: string;\n}\n\n/** One change's renders, divided by what happened to each. */\nexport interface Parted {\n readonly shapes: readonly Shape[];\n /**\n * Renders whose leading region recorded no fingerprint.\n *\n * Their own list, never a shape. Two renders that both recorded nothing have\n * not been found to agree, and folding them together would report the\n * identical difference twice about a pair nothing measured.\n */\n readonly unshaped: readonly string[];\n}\n\nexport function partedBy(origin: Origin): Parted {\n const groups = new Map<string, Appearance[]>();\n const unshaped: string[] = [];\n\n for (const each of origin.appearances) {\n if (each.shape === undefined) {\n unshaped.push(each.subject.subject);\n continue;\n }\n groups.set(each.shape, [...(groups.get(each.shape) ?? []), each]);\n }\n\n const found = [...groups].map(([shape, members]) => {\n const pixels = members.map((each) => each.pixels);\n return {\n shape,\n renders: members.map((each) => each.subject.subject).sort(),\n least: Math.min(...pixels),\n most: Math.max(...pixels),\n };\n });\n\n const shapes = found\n .map((group) => {\n const only = nameOf(\n group.renders,\n found.filter((other) => other.shape !== group.shape).flatMap((other) => other.renders),\n );\n return only === undefined ? group : { ...group, only };\n })\n .sort((left, right) => right.most - left.most || left.shape.localeCompare(right.shape));\n\n return { shapes, unshaped: unshaped.sort() };\n}\n\n/** Boundaries a subject name is built from, so a prefix is cut at a joint. */\nconst JOINTS = new Set(['/', '@', ':', '-', '.', '_']);\n\n/**\n * The name these renders share, when no other group's renders share it.\n *\n * `undefined` for a single render, where the prefix would be the subject itself\n * and printing it beside the subject says nothing twice.\n *\n * The common prefix is only a name when it ends where the renders do — at a\n * joint, or at the end of one of them. `route/detail@1280` and\n * `route/detail@1024` have `route/detail@1` in common and no such viewport\n * exists; the name is `route/detail`, which is then shared with the other group\n * and so is not printed at all.\n */\nfunction nameOf(renders: readonly string[], others: readonly string[]): string | undefined {\n if (renders.length < 2) return undefined;\n\n const [first = '', ...rest] = renders;\n let cut = first.length;\n for (const each of rest) {\n let index = 0;\n while (index < cut && index < each.length && each[index] === first[index]) index += 1;\n cut = index;\n }\n\n if (!renders.every((each) => each.length === cut || JOINTS.has(each[cut] ?? ''))) {\n while (cut > 0 && !JOINTS.has(first[cut - 1] ?? '')) cut -= 1;\n }\n while (cut > 0 && JOINTS.has(first[cut - 1] ?? '')) cut -= 1;\n const shared = first.slice(0, cut);\n if (shared === '') return undefined;\n\n return others.some((each) => each.startsWith(shared)) ? undefined : shared;\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rail: every subject worth a decision, grouped by verdict.
|
|
3
|
+
*
|
|
4
|
+
* Apart from [`review.tsx`](./review.tsx) because it is the one control the page
|
|
5
|
+
* is navigated by and it answers a question of its own — *which of these is worth
|
|
6
|
+
* opening* — before any render is fetched. The note under each name is the
|
|
7
|
+
* component the semantic tier blamed and the size of the difference, which is
|
|
8
|
+
* enough to decide without a round trip.
|
|
9
|
+
*
|
|
10
|
+
* What it does not carry is anything green. That is [`settled.tsx`](./settled.js)'s,
|
|
11
|
+
* and the split is the point: a rail that listed a subject an ignore had already
|
|
12
|
+
* decided would put it in a queue, under a header counting what awaits review.
|
|
13
|
+
*/
|
|
14
|
+
import type { ReactElement } from 'react';
|
|
15
|
+
import type { SubjectView } from '../review-types.js';
|
|
16
|
+
/**
|
|
17
|
+
* Every subject worth a decision, grouped by verdict and readable at a glance.
|
|
18
|
+
*
|
|
19
|
+
* The rail exists so that choosing what to look at costs a glance rather than a
|
|
20
|
+
* scroll: the note under each name is the component the tier blamed and the size
|
|
21
|
+
* of the difference, which is enough to decide whether this one needs opening at
|
|
22
|
+
* all. Sorted largest-first inside each group, for the same reason the docket is.
|
|
23
|
+
*/
|
|
24
|
+
export declare function SubjectRail({ subjects, causes, variations, selected, onSelect, }: {
|
|
25
|
+
readonly subjects: readonly SubjectView[];
|
|
26
|
+
readonly causes: number;
|
|
27
|
+
readonly variations: number;
|
|
28
|
+
readonly selected: string | null;
|
|
29
|
+
readonly onSelect: (subject: string | null) => void;
|
|
30
|
+
}): ReactElement;
|
|
31
|
+
//# sourceMappingURL=rail.d.ts.map
|
package/dist/ui/rail.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { causeOf } from './lead.js';
|
|
3
|
+
import { briefly, count } from './text.js';
|
|
4
|
+
/**
|
|
5
|
+
* Every subject worth a decision, grouped by verdict and readable at a glance.
|
|
6
|
+
*
|
|
7
|
+
* The rail exists so that choosing what to look at costs a glance rather than a
|
|
8
|
+
* scroll: the note under each name is the component the tier blamed and the size
|
|
9
|
+
* of the difference, which is enough to decide whether this one needs opening at
|
|
10
|
+
* all. Sorted largest-first inside each group, for the same reason the docket is.
|
|
11
|
+
*/
|
|
12
|
+
export function SubjectRail({ subjects, causes, variations, selected, onSelect, }) {
|
|
13
|
+
const groups = [...new Set(subjects.map((subject) => subject.verdict))];
|
|
14
|
+
return (_jsxs("div", { className: "va-rail-list va-scroll", children: [_jsx("button", { type: "button", className: selected === null ? 'va-rail-item va-current' : 'va-rail-item', onClick: () => onSelect(null), children: _jsxs("span", { className: "va-rail-body", children: [_jsx("span", { className: "va-rail-name", children: "The docket" }), _jsxs("span", { className: "va-rail-note", children: [count(causes, 'cause'), " \u00B7 ", count(variations, 'variation')] })] }) }), groups.map((verdict) => {
|
|
15
|
+
const rows = subjects
|
|
16
|
+
.filter((each) => each.verdict === verdict)
|
|
17
|
+
.sort((left, right) => right.changedPixels - left.changedPixels);
|
|
18
|
+
return (_jsxs("section", { children: [_jsxs("h2", { className: "va-rail-group", children: [verdict, _jsx("span", { className: "va-rail-count va-num", children: rows.length })] }), _jsx("ul", { children: rows.map((subject) => (_jsx("li", { children: _jsxs("button", { type: "button", className: subject.subject === selected ? 'va-rail-item va-current' : 'va-rail-item', onClick: () => onSelect(subject.subject), children: [_jsx("span", { className: `va-dot va-${subject.verdict}` }), _jsxs("span", { className: "va-rail-body", children: [_jsx("span", { className: "va-rail-name", children: subject.subject }), _jsxs("span", { className: "va-rail-note", children: [causeOf(subject) ?? 'no component named', subject.changedPixels > 0 ? ` · ${briefly(subject)}` : ''] })] }), subject.decision === null ? null : (_jsx("span", { className: `va-mark va-${subject.decision.decision}`, children: subject.decision.decision === 'approved' ? '✓' : '✗' }))] }) }, subject.subject))) })] }, verdict));
|
|
19
|
+
})] }));
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=rail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rail.js","sourceRoot":"","sources":["../../src/ui/rail.tsx"],"names":[],"mappings":";AAgBA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,QAAQ,EACR,MAAM,EACN,UAAU,EACV,QAAQ,EACR,QAAQ,GAOT;IACC,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAExE,OAAO,CACL,eAAK,SAAS,EAAC,wBAAwB,aACrC,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,cAAc,EACzE,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,YAE7B,gBAAM,SAAS,EAAC,cAAc,aAC5B,eAAM,SAAS,EAAC,cAAc,2BAAkB,EAChD,gBAAM,SAAS,EAAC,cAAc,aAC3B,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,cAAK,KAAK,CAAC,UAAU,EAAE,WAAW,CAAC,IACrD,IACF,GACA,EAER,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;gBACtB,MAAM,IAAI,GAAG,QAAQ;qBAClB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,KAAK,OAAO,CAAC;qBAC1C,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC;gBAEnE,OAAO,CACL,8BACE,cAAI,SAAS,EAAC,eAAe,aAC1B,OAAO,EACR,eAAM,SAAS,EAAC,sBAAsB,YAAE,IAAI,CAAC,MAAM,GAAQ,IACxD,EACL,uBACG,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CACrB,uBACE,kBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EACP,OAAO,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,cAAc,EAE3E,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,aAExC,eAAM,SAAS,EAAE,aAAa,OAAO,CAAC,OAAO,EAAE,GAAI,EACnD,gBAAM,SAAS,EAAC,cAAc,aAC5B,eAAM,SAAS,EAAC,cAAc,YAAE,OAAO,CAAC,OAAO,GAAQ,EACvD,gBAAM,SAAS,EAAC,cAAc,aAC3B,OAAO,CAAC,OAAO,CAAC,IAAI,oBAAoB,EACxC,OAAO,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IACrD,IACF,EACN,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAClC,eAAM,SAAS,EAAE,cAAc,OAAO,CAAC,QAAQ,CAAC,QAAQ,EAAE,YACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAChD,CACR,IACM,IArBF,OAAO,CAAC,OAAO,CAsBnB,CACN,CAAC,GACC,KA/BO,OAAO,CAgCX,CACX,CAAC;YACJ,CAAC,CAAC,IACE,CACP,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The rail: every subject worth a decision, grouped by verdict.\n *\n * Apart from [`review.tsx`](./review.tsx) because it is the one control the page\n * is navigated by and it answers a question of its own — *which of these is worth\n * opening* — before any render is fetched. The note under each name is the\n * component the semantic tier blamed and the size of the difference, which is\n * enough to decide without a round trip.\n *\n * What it does not carry is anything green. That is [`settled.tsx`](./settled.js)'s,\n * and the split is the point: a rail that listed a subject an ignore had already\n * decided would put it in a queue, under a header counting what awaits review.\n */\n\nimport type { ReactElement } from 'react';\nimport type { SubjectView } from '../review-types.js';\nimport { causeOf } from './lead.js';\nimport { briefly, count } from './text.js';\n\n/**\n * Every subject worth a decision, grouped by verdict and readable at a glance.\n *\n * The rail exists so that choosing what to look at costs a glance rather than a\n * scroll: the note under each name is the component the tier blamed and the size\n * of the difference, which is enough to decide whether this one needs opening at\n * all. Sorted largest-first inside each group, for the same reason the docket is.\n */\nexport function SubjectRail({\n subjects,\n causes,\n variations,\n selected,\n onSelect,\n}: {\n readonly subjects: readonly SubjectView[];\n readonly causes: number;\n readonly variations: number;\n readonly selected: string | null;\n readonly onSelect: (subject: string | null) => void;\n}): ReactElement {\n const groups = [...new Set(subjects.map((subject) => subject.verdict))];\n\n return (\n <div className=\"va-rail-list va-scroll\">\n <button\n type=\"button\"\n className={selected === null ? 'va-rail-item va-current' : 'va-rail-item'}\n onClick={() => onSelect(null)}\n >\n <span className=\"va-rail-body\">\n <span className=\"va-rail-name\">The docket</span>\n <span className=\"va-rail-note\">\n {count(causes, 'cause')} · {count(variations, 'variation')}\n </span>\n </span>\n </button>\n\n {groups.map((verdict) => {\n const rows = subjects\n .filter((each) => each.verdict === verdict)\n .sort((left, right) => right.changedPixels - left.changedPixels);\n\n return (\n <section key={verdict}>\n <h2 className=\"va-rail-group\">\n {verdict}\n <span className=\"va-rail-count va-num\">{rows.length}</span>\n </h2>\n <ul>\n {rows.map((subject) => (\n <li key={subject.subject}>\n <button\n type=\"button\"\n className={\n subject.subject === selected ? 'va-rail-item va-current' : 'va-rail-item'\n }\n onClick={() => onSelect(subject.subject)}\n >\n <span className={`va-dot va-${subject.verdict}`} />\n <span className=\"va-rail-body\">\n <span className=\"va-rail-name\">{subject.subject}</span>\n <span className=\"va-rail-note\">\n {causeOf(subject) ?? 'no component named'}\n {subject.changedPixels > 0 ? ` · ${briefly(subject)}` : ''}\n </span>\n </span>\n {subject.decision === null ? null : (\n <span className={`va-mark va-${subject.decision.decision}`}>\n {subject.decision.decision === 'approved' ? '✓' : '✗'}\n </span>\n )}\n </button>\n </li>\n ))}\n </ul>\n </section>\n );\n })}\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the commit reaches, crossed against what the run saw.
|
|
3
|
+
*
|
|
4
|
+
* Every other panel on this page answers *what changed*. This one answers *what
|
|
5
|
+
* could have*, and the whole value is in the crossing: a verdict alone cannot say
|
|
6
|
+
* whether a green subject was supposed to be green, and a diff alone cannot say
|
|
7
|
+
* whether the file somebody edited paints anything at all.
|
|
8
|
+
*
|
|
9
|
+
* Four states come out of it, and two are findings no image comparison can reach:
|
|
10
|
+
*
|
|
11
|
+
* - **reached, and moved** — the expected case, and the trail names the file.
|
|
12
|
+
* - **reached, and still** — the commit reaches this subject and changed no pixel
|
|
13
|
+
* in it. Every tool in this category is silent about a green subject, so an
|
|
14
|
+
* author who believed they were changing this surface learns it here or not at
|
|
15
|
+
* all.
|
|
16
|
+
* - **moved, unreached** — nothing in the commit arrives here and it moved anyway.
|
|
17
|
+
* That is a flake, an input from outside the repository, or a hole in the scan,
|
|
18
|
+
* and only the record separates them — so the record is read without waiting to
|
|
19
|
+
* be asked.
|
|
20
|
+
* - **untouched, and still** — counted, never listed. On a narrowed run it reads
|
|
21
|
+
* *untouched, not rendered* instead, because those subjects were ruled out
|
|
22
|
+
* before any browser opened them and were never compared to anything.
|
|
23
|
+
*
|
|
24
|
+
* ## A refusal is drawn instead of the grid, not beside it
|
|
25
|
+
*
|
|
26
|
+
* A run can carry a diff it could not attribute: a changed file the graph does not
|
|
27
|
+
* hold, a diff with nothing in the graph at all, a diff arriving at no component.
|
|
28
|
+
* The store keeps that apart from an attribution that genuinely reached nothing,
|
|
29
|
+
* and so does this. An empty grid over a refusal reads as *this commit reaches
|
|
30
|
+
* none of your subjects*, which is a sentence somebody merges on.
|
|
31
|
+
*/
|
|
32
|
+
import { type ReactElement } from 'react';
|
|
33
|
+
import type { BuildDetail, ReachView, SubjectView } from '../review-types.js';
|
|
34
|
+
import type { ReviewClient } from './client.js';
|
|
35
|
+
/**
|
|
36
|
+
* The four states, and the two counts that keep them honest.
|
|
37
|
+
*
|
|
38
|
+
* `unplaced` and `incomparable` exist so the four quadrants add up to the suite
|
|
39
|
+
* without any of them absorbing a subject nobody can classify. A baseline that
|
|
40
|
+
* recorded no component list cannot be put on either axis, and a subject that is
|
|
41
|
+
* new or that nothing could compare has no *moved* to speak of. Folding either
|
|
42
|
+
* into the quiet quadrant would make the one nobody reads the place uncertainty
|
|
43
|
+
* goes to die.
|
|
44
|
+
*/
|
|
45
|
+
export interface Crossing {
|
|
46
|
+
readonly reachedMoved: readonly SubjectView[];
|
|
47
|
+
readonly reachedStill: readonly SubjectView[];
|
|
48
|
+
readonly unreachedMoved: readonly SubjectView[];
|
|
49
|
+
readonly unreachedStill: number;
|
|
50
|
+
/**
|
|
51
|
+
* Out of reach and never rendered, because the run worked that out first.
|
|
52
|
+
*
|
|
53
|
+
* Apart from `unreachedStill` and never added to it. Those were opened and
|
|
54
|
+
* found identical; these were never opened at all, and a page that summed them
|
|
55
|
+
* would report a comparison for eighteen subjects no browser ever loaded.
|
|
56
|
+
*/
|
|
57
|
+
readonly unreachedSkipped: number;
|
|
58
|
+
readonly unplaced: number;
|
|
59
|
+
readonly incomparable: number;
|
|
60
|
+
}
|
|
61
|
+
export declare function crossReach(subjects: readonly SubjectView[], reach: ReachView, notObserved?: BuildDetail['notObserved']): Crossing;
|
|
62
|
+
export declare function ReachPanel({ client, build, }: {
|
|
63
|
+
readonly client: ReviewClient;
|
|
64
|
+
readonly build: BuildDetail;
|
|
65
|
+
}): ReactElement | null;
|
|
66
|
+
//# sourceMappingURL=reach.d.ts.map
|
package/dist/ui/reach.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* What the commit reaches, crossed against what the run saw.
|
|
4
|
+
*
|
|
5
|
+
* Every other panel on this page answers *what changed*. This one answers *what
|
|
6
|
+
* could have*, and the whole value is in the crossing: a verdict alone cannot say
|
|
7
|
+
* whether a green subject was supposed to be green, and a diff alone cannot say
|
|
8
|
+
* whether the file somebody edited paints anything at all.
|
|
9
|
+
*
|
|
10
|
+
* Four states come out of it, and two are findings no image comparison can reach:
|
|
11
|
+
*
|
|
12
|
+
* - **reached, and moved** — the expected case, and the trail names the file.
|
|
13
|
+
* - **reached, and still** — the commit reaches this subject and changed no pixel
|
|
14
|
+
* in it. Every tool in this category is silent about a green subject, so an
|
|
15
|
+
* author who believed they were changing this surface learns it here or not at
|
|
16
|
+
* all.
|
|
17
|
+
* - **moved, unreached** — nothing in the commit arrives here and it moved anyway.
|
|
18
|
+
* That is a flake, an input from outside the repository, or a hole in the scan,
|
|
19
|
+
* and only the record separates them — so the record is read without waiting to
|
|
20
|
+
* be asked.
|
|
21
|
+
* - **untouched, and still** — counted, never listed. On a narrowed run it reads
|
|
22
|
+
* *untouched, not rendered* instead, because those subjects were ruled out
|
|
23
|
+
* before any browser opened them and were never compared to anything.
|
|
24
|
+
*
|
|
25
|
+
* ## A refusal is drawn instead of the grid, not beside it
|
|
26
|
+
*
|
|
27
|
+
* A run can carry a diff it could not attribute: a changed file the graph does not
|
|
28
|
+
* hold, a diff with nothing in the graph at all, a diff arriving at no component.
|
|
29
|
+
* The store keeps that apart from an attribution that genuinely reached nothing,
|
|
30
|
+
* and so does this. An empty grid over a refusal reads as *this commit reaches
|
|
31
|
+
* none of your subjects*, which is a sentence somebody merges on.
|
|
32
|
+
*/
|
|
33
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
34
|
+
import { StabilityLine } from './history.js';
|
|
35
|
+
import { OutcomeMapView } from './outcome.js';
|
|
36
|
+
import { briefly, count, number } from './text.js';
|
|
37
|
+
export function crossReach(subjects, reach, notObserved = []) {
|
|
38
|
+
const placed = reach.subjects;
|
|
39
|
+
const reachedMoved = [];
|
|
40
|
+
const reachedStill = [];
|
|
41
|
+
const unreachedMoved = [];
|
|
42
|
+
let unreachedStill = 0;
|
|
43
|
+
let unplaced = 0;
|
|
44
|
+
let incomparable = 0;
|
|
45
|
+
for (const subject of subjects) {
|
|
46
|
+
const entry = placed?.[subject.subject];
|
|
47
|
+
if (entry === undefined) {
|
|
48
|
+
unplaced += 1;
|
|
49
|
+
}
|
|
50
|
+
else if (subject.verdict === 'changed') {
|
|
51
|
+
(entry.reached ? reachedMoved : unreachedMoved).push(subject);
|
|
52
|
+
}
|
|
53
|
+
else if (subject.verdict === 'unchanged') {
|
|
54
|
+
// `changed` and `unchanged` are the only verdicts describing a pair this
|
|
55
|
+
// run actually compared. A new subject has no baseline to have moved from,
|
|
56
|
+
// and an incomparable one is a comparison that did not happen — neither
|
|
57
|
+
// belongs on an axis called moved.
|
|
58
|
+
if (entry.reached)
|
|
59
|
+
reachedStill.push(subject);
|
|
60
|
+
else
|
|
61
|
+
unreachedStill += 1;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
incomparable += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
reachedMoved,
|
|
69
|
+
reachedStill,
|
|
70
|
+
unreachedMoved,
|
|
71
|
+
unreachedStill,
|
|
72
|
+
unreachedSkipped: notObserved.filter((entry) => entry.kind === 'unreached').length,
|
|
73
|
+
unplaced,
|
|
74
|
+
incomparable,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** How many unexplained subjects read their own record without being asked. */
|
|
78
|
+
const ASKED = 5;
|
|
79
|
+
/** How many trails are drawn before the rest are counted. */
|
|
80
|
+
const TRAILS = 8;
|
|
81
|
+
export function ReachPanel({ client, build, }) {
|
|
82
|
+
const reach = build.reach;
|
|
83
|
+
if (reach === null)
|
|
84
|
+
return null;
|
|
85
|
+
return (_jsxs("section", { className: "va-card va-reach", children: [_jsx("h2", { children: "What this commit reaches" }), _jsxs("p", { className: "va-subtitle", children: [count(reach.changed.length, 'file'), " changed against ", _jsx("code", { children: reach.against }), ". The import graph carries them to ", count(reach.components.length, 'component'), "; the baselines say which subjects render those."] }), _jsx(OutcomeMapView, { build: build }), reach.whole === undefined ? (_jsx(Crossed, { client: client, build: build, reach: reach })) : (_jsxs("p", { className: "va-refusal", children: ["This diff was not attributed, so nothing here may be called unreached: ", reach.whole, "."] })), _jsx(Holes, { reach: reach })] }));
|
|
86
|
+
}
|
|
87
|
+
function Crossed({ client, build, reach, }) {
|
|
88
|
+
const crossing = crossReach(build.subjects, reach, build.notObserved);
|
|
89
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "va-quad", children: [_jsx(Cell, { tone: "va-expected", n: crossing.reachedMoved.length, title: "reached, and moved", note: "The commit arrives and the render changed. What the edit was for." }), _jsx(Cell, { tone: "va-alarm", n: crossing.unreachedMoved.length, title: "moved, unreached", note: "Nothing in this commit arrives here. It moved anyway." }), _jsx(Cell, { tone: "va-inert", n: crossing.reachedStill.length, title: "reached, and still", note: "The commit arrives and changed no pixel of it." }), crossing.unreachedSkipped === 0 ? (_jsx(Cell, { tone: "va-quiet", n: crossing.unreachedStill, title: "untouched, and still", note: "Out of the commit's reach, and unchanged." })) : (_jsx(Cell, { tone: "va-quiet", n: crossing.unreachedSkipped, title: "untouched, not rendered", note: "Out of the commit's reach, so this run never opened them." }))] }), crossing.unreachedMoved.length === 0 ? null : (_jsxs("section", { className: "va-band va-alarm", children: [_jsx("h3", { children: "Moved with nothing reaching it" }), _jsx("p", { className: "va-note", children: "The graph placed every changed file and none of them arrives here. Either this run read the subject differently from the last, something outside the repository moved, or the scan has a hole in it \u2014 and the record is what tells those apart." }), _jsx("ul", { className: "va-reach-list", children: crossing.unreachedMoved.map((subject, index) => (_jsxs("li", { children: [_jsx("strong", { children: subject.subject }), ' ', _jsxs("span", { className: "va-note", children: [briefly(subject), " moved"] }), index < ASKED ? _jsx(Journey, { client: client, subject: subject.subject }) : null] }, subject.subject))) }), crossing.unreachedMoved.length <= ASKED ? null : (_jsxs("p", { className: "va-note", children: ["The record was read for the first ", number(ASKED), ". Open the rest from the rail."] }))] })), crossing.reachedStill.length === 0 ? null : (_jsxs("section", { className: "va-band va-inert", children: [_jsx("h3", { children: "Reached, and did not move" }), _jsx("p", { className: "va-note", children: "The commit arrives at these and the render is identical to the baseline. Nothing else on this page says anything at all about a green subject." }), _jsx("ul", { className: "va-reach-list va-columns", children: crossing.reachedStill.map((subject) => (_jsxs("li", { children: [_jsx("strong", { children: subject.subject }), ' ', _jsx(Via, { through: reach.subjects?.[subject.subject]?.through ?? [] })] }, subject.subject))) })] })), _jsx(Trails, { reach: reach }), _jsx(Unplaced, { crossing: crossing })] }));
|
|
90
|
+
}
|
|
91
|
+
function Cell({ tone, n, title, note, }) {
|
|
92
|
+
// A zero is drawn quietly rather than hidden. An omitted quadrant reads as a
|
|
93
|
+
// question that was not asked; a zero is the answer to it.
|
|
94
|
+
return (_jsxs("div", { className: n === 0 ? `va-cell ${tone} va-zero` : `va-cell ${tone}`, children: [_jsx("span", { className: "va-cell-n va-num", children: number(n) }), _jsx("span", { className: "va-cell-title", children: title }), _jsx("span", { className: "va-cell-note", children: note })] }));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The chains, which are the part a reviewer can check.
|
|
98
|
+
*
|
|
99
|
+
* A component named without its chain is an assertion. `tokens.css →
|
|
100
|
+
* button.tsx → Button` is a claim somebody can open three files and disprove,
|
|
101
|
+
* which is the only kind worth putting on a review surface.
|
|
102
|
+
*/
|
|
103
|
+
function Trails({ reach }) {
|
|
104
|
+
if (reach.components.length === 0)
|
|
105
|
+
return null;
|
|
106
|
+
const shown = reach.components.slice(0, TRAILS);
|
|
107
|
+
return (_jsxs("section", { className: "va-band", children: [_jsx("h3", { children: "How the diff arrives" }), _jsx("ul", { className: "va-trails", children: shown.map((entry) => (_jsxs("li", { children: [entry.trail.map((step, index) => (_jsxs("span", { children: [index === 0 ? null : _jsx("span", { className: "va-arrow", children: "\u2192" }), index === entry.trail.length - 1 ? _jsx("strong", { children: step }) : _jsx("code", { children: step })] }, step))), entry.throughUnread === undefined ? null : (_jsx("span", { className: "va-axis va-unread", children: "begins at a file the scan could not read" }))] }, entry.component))) }), reach.components.length <= shown.length ? null : (_jsxs("p", { className: "va-note", children: [count(reach.components.length - shown.length, 'further component'), " reached, not listed."] }))] }));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The subjects the crossing could not seat, said rather than absorbed.
|
|
111
|
+
*
|
|
112
|
+
* Two different silences, kept as two. A subject whose baseline carries no
|
|
113
|
+
* component list is one this surface cannot answer for; a new or incomparable
|
|
114
|
+
* subject is one where the question does not apply. Printing a single number for
|
|
115
|
+
* both would invite the reader to fix the wrong thing.
|
|
116
|
+
*/
|
|
117
|
+
function Unplaced({ crossing }) {
|
|
118
|
+
if (crossing.unplaced + crossing.incomparable === 0)
|
|
119
|
+
return null;
|
|
120
|
+
return (_jsxs("p", { className: "va-note va-reach-note", children: [crossing.unplaced === 0 ? null : (_jsxs(_Fragment, { children: [count(crossing.unplaced, 'subject'), " could not be placed on either axis, because", ' ', crossing.unplaced === 1 ? 'its baseline records' : 'their baselines record', " no component list.", ' '] })), crossing.incomparable === 0 ? null : (_jsxs(_Fragment, { children: [count(crossing.incomparable, 'subject'), ' ', crossing.incomparable === 1 ? 'was' : 'were', " new or could not be compared, so there is no moved-or-still to cross."] }))] }));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Where the attribution is knowingly wider than the diff.
|
|
124
|
+
*
|
|
125
|
+
* Both lists name their files rather than counting them, because both are things
|
|
126
|
+
* an operator can go and fix, and a bare count tells them there is nothing to do.
|
|
127
|
+
*/
|
|
128
|
+
function Holes({ reach }) {
|
|
129
|
+
if (reach.unscanned === undefined && reach.opaque === undefined)
|
|
130
|
+
return null;
|
|
131
|
+
return (_jsxs("p", { className: "va-note va-reach-note", children: [reach.unscanned === undefined ? null : (_jsxs(_Fragment, { children: [count(reach.unscanned.length, 'changed file'), " under the scanned roots", ' ', reach.unscanned.length === 1 ? 'is' : 'are', " not in the graph:", ' ', _jsx(Files, { names: reach.unscanned }), ".", ' '] })), reach.opaque === undefined ? null : (_jsxs(_Fragment, { children: [count(reach.opaque.length, 'file'), " was walked as changed because its own imports could not be read: ", _jsx(Files, { names: reach.opaque.map((hole) => hole.file) }), "."] }))] }));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Which of a subject's own components the commit arrives at.
|
|
135
|
+
*
|
|
136
|
+
* `SubjectReach.through` holds component names, not paths, and they are set as
|
|
137
|
+
* prose for that reason: a reader shown `MainNav` in the same monospace face as
|
|
138
|
+
* `app/src/components/MainNav.tsx` reads the two as the same kind of thing and
|
|
139
|
+
* starts looking for a file by that name.
|
|
140
|
+
*
|
|
141
|
+
* Nothing at all when the list is empty, rather than a dangling `via`. Empty here
|
|
142
|
+
* is `reached: false`, which this list is not.
|
|
143
|
+
*/
|
|
144
|
+
function Via({ through }) {
|
|
145
|
+
if (through.length === 0)
|
|
146
|
+
return null;
|
|
147
|
+
return _jsxs("span", { className: "va-note", children: ["via ", through.join(', ')] });
|
|
148
|
+
}
|
|
149
|
+
function Files({ names }) {
|
|
150
|
+
return (_jsx(_Fragment, { children: names.map((name, index) => (_jsxs("span", { children: [index === 0 ? null : ', ', _jsx("code", { children: name })] }, name))) }));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* What the record says about a subject nothing in this commit reaches.
|
|
154
|
+
*
|
|
155
|
+
* Read on mount rather than behind a button, which is the opposite of the
|
|
156
|
+
* per-subject history elsewhere on this page and is deliberate. A reviewer who
|
|
157
|
+
* has opened a subject has already decided it is worth their time; this list is
|
|
158
|
+
* short by construction and every row on it is *already* the anomaly. "Has this
|
|
159
|
+
* moved on its own before" is the entire reason the row is here, so waiting to be
|
|
160
|
+
* asked it would be waiting to be asked the point.
|
|
161
|
+
*/
|
|
162
|
+
function Journey({ client, subject, }) {
|
|
163
|
+
const [answer, setAnswer] = useState({ state: 'reading' });
|
|
164
|
+
const load = useCallback(async () => {
|
|
165
|
+
try {
|
|
166
|
+
setAnswer({ state: 'read', flakiness: await client.flakiness(subject) });
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
setAnswer({ state: 'failed', why: error instanceof Error ? error.message : String(error) });
|
|
170
|
+
}
|
|
171
|
+
}, [client, subject]);
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
void load();
|
|
174
|
+
}, [load]);
|
|
175
|
+
if (answer.state === 'reading')
|
|
176
|
+
return _jsx("p", { className: "va-note", children: "Reading the record\u2026" });
|
|
177
|
+
// A record that could not be read says so. Blank here reads as "nothing on
|
|
178
|
+
// file", which is the one answer that would send a reviewer to approve an
|
|
179
|
+
// otherwise unexplained change.
|
|
180
|
+
if (answer.state === 'failed') {
|
|
181
|
+
return _jsxs("p", { className: "va-note va-lost", children: ["The record could not be read: ", answer.why] });
|
|
182
|
+
}
|
|
183
|
+
return _jsx(StabilityLine, { flakiness: answer.flakiness });
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=reach.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reach.js","sourceRoot":"","sources":["../../src/ui/reach.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the commit reaches, crossed against what the run saw.\n *\n * Every other panel on this page answers *what changed*. This one answers *what\n * could have*, and the whole value is in the crossing: a verdict alone cannot say\n * whether a green subject was supposed to be green, and a diff alone cannot say\n * whether the file somebody edited paints anything at all.\n *\n * Four states come out of it, and two are findings no image comparison can reach:\n *\n * - **reached, and moved** — the expected case, and the trail names the file.\n * - **reached, and still** — the commit reaches this subject and changed no pixel\n * in it. Every tool in this category is silent about a green subject, so an\n * author who believed they were changing this surface learns it here or not at\n * all.\n * - **moved, unreached** — nothing in the commit arrives here and it moved anyway.\n * That is a flake, an input from outside the repository, or a hole in the scan,\n * and only the record separates them — so the record is read without waiting to\n * be asked.\n * - **untouched, and still** — counted, never listed. On a narrowed run it reads\n * *untouched, not rendered* instead, because those subjects were ruled out\n * before any browser opened them and were never compared to anything.\n *\n * ## A refusal is drawn instead of the grid, not beside it\n *\n * A run can carry a diff it could not attribute: a changed file the graph does not\n * hold, a diff with nothing in the graph at all, a diff arriving at no component.\n * The store keeps that apart from an attribution that genuinely reached nothing,\n * and so does this. An empty grid over a refusal reads as *this commit reaches\n * none of your subjects*, which is a sentence somebody merges on.\n */\n\nimport { useCallback, useEffect, useState, type ReactElement } from 'react';\nimport type { Flakiness } from '@variance-authority/history';\nimport type { BuildDetail, ReachView, SubjectView } from '../review-types.js';\nimport type { ReviewClient } from './client.js';\nimport { StabilityLine } from './history.js';\nimport { OutcomeMapView } from './outcome.js';\nimport { briefly, count, number } from './text.js';\n\n/**\n * The four states, and the two counts that keep them honest.\n *\n * `unplaced` and `incomparable` exist so the four quadrants add up to the suite\n * without any of them absorbing a subject nobody can classify. A baseline that\n * recorded no component list cannot be put on either axis, and a subject that is\n * new or that nothing could compare has no *moved* to speak of. Folding either\n * into the quiet quadrant would make the one nobody reads the place uncertainty\n * goes to die.\n */\nexport interface Crossing {\n readonly reachedMoved: readonly SubjectView[];\n readonly reachedStill: readonly SubjectView[];\n readonly unreachedMoved: readonly SubjectView[];\n readonly unreachedStill: number;\n /**\n * Out of reach and never rendered, because the run worked that out first.\n *\n * Apart from `unreachedStill` and never added to it. Those were opened and\n * found identical; these were never opened at all, and a page that summed them\n * would report a comparison for eighteen subjects no browser ever loaded.\n */\n readonly unreachedSkipped: number;\n readonly unplaced: number;\n readonly incomparable: number;\n}\n\nexport function crossReach(\n subjects: readonly SubjectView[],\n reach: ReachView,\n notObserved: BuildDetail['notObserved'] = [],\n): Crossing {\n const placed = reach.subjects;\n const reachedMoved: SubjectView[] = [];\n const reachedStill: SubjectView[] = [];\n const unreachedMoved: SubjectView[] = [];\n let unreachedStill = 0;\n let unplaced = 0;\n let incomparable = 0;\n\n for (const subject of subjects) {\n const entry = placed?.[subject.subject];\n if (entry === undefined) {\n unplaced += 1;\n } else if (subject.verdict === 'changed') {\n (entry.reached ? reachedMoved : unreachedMoved).push(subject);\n } else if (subject.verdict === 'unchanged') {\n // `changed` and `unchanged` are the only verdicts describing a pair this\n // run actually compared. A new subject has no baseline to have moved from,\n // and an incomparable one is a comparison that did not happen — neither\n // belongs on an axis called moved.\n if (entry.reached) reachedStill.push(subject);\n else unreachedStill += 1;\n } else {\n incomparable += 1;\n }\n }\n\n return {\n reachedMoved,\n reachedStill,\n unreachedMoved,\n unreachedStill,\n unreachedSkipped: notObserved.filter((entry) => entry.kind === 'unreached').length,\n unplaced,\n incomparable,\n };\n}\n\n/** How many unexplained subjects read their own record without being asked. */\nconst ASKED = 5;\n\n/** How many trails are drawn before the rest are counted. */\nconst TRAILS = 8;\n\nexport function ReachPanel({\n client,\n build,\n}: {\n readonly client: ReviewClient;\n readonly build: BuildDetail;\n}): ReactElement | null {\n const reach = build.reach;\n if (reach === null) return null;\n\n return (\n <section className=\"va-card va-reach\">\n <h2>What this commit reaches</h2>\n <p className=\"va-subtitle\">\n {count(reach.changed.length, 'file')} changed against <code>{reach.against}</code>. The\n import graph carries them to {count(reach.components.length, 'component')}; the baselines say\n which subjects render those.\n </p>\n\n <OutcomeMapView build={build} />\n\n {reach.whole === undefined ? (\n <Crossed client={client} build={build} reach={reach} />\n ) : (\n <p className=\"va-refusal\">\n This diff was not attributed, so nothing here may be called unreached: {reach.whole}.\n </p>\n )}\n\n <Holes reach={reach} />\n </section>\n );\n}\n\nfunction Crossed({\n client,\n build,\n reach,\n}: {\n readonly client: ReviewClient;\n readonly build: BuildDetail;\n readonly reach: ReachView;\n}): ReactElement {\n const crossing = crossReach(build.subjects, reach, build.notObserved);\n\n return (\n <>\n <div className=\"va-quad\">\n <Cell\n tone=\"va-expected\"\n n={crossing.reachedMoved.length}\n title=\"reached, and moved\"\n note=\"The commit arrives and the render changed. What the edit was for.\"\n />\n <Cell\n tone=\"va-alarm\"\n n={crossing.unreachedMoved.length}\n title=\"moved, unreached\"\n note=\"Nothing in this commit arrives here. It moved anyway.\"\n />\n <Cell\n tone=\"va-inert\"\n n={crossing.reachedStill.length}\n title=\"reached, and still\"\n note=\"The commit arrives and changed no pixel of it.\"\n />\n {crossing.unreachedSkipped === 0 ? (\n <Cell\n tone=\"va-quiet\"\n n={crossing.unreachedStill}\n title=\"untouched, and still\"\n note=\"Out of the commit's reach, and unchanged.\"\n />\n ) : (\n // The run narrowed to what the diff can arrive at, so these were never\n // opened. Drawn as *not rendered* rather than *unchanged*: nothing\n // compared them, and the quadrant that says otherwise would be the one\n // place on the page claiming a result for work nobody did.\n <Cell\n tone=\"va-quiet\"\n n={crossing.unreachedSkipped}\n title=\"untouched, not rendered\"\n note=\"Out of the commit's reach, so this run never opened them.\"\n />\n )}\n </div>\n\n {crossing.unreachedMoved.length === 0 ? null : (\n <section className=\"va-band va-alarm\">\n <h3>Moved with nothing reaching it</h3>\n <p className=\"va-note\">\n The graph placed every changed file and none of them arrives here. Either this run read\n the subject differently from the last, something outside the repository moved, or the\n scan has a hole in it — and the record is what tells those apart.\n </p>\n <ul className=\"va-reach-list\">\n {crossing.unreachedMoved.map((subject, index) => (\n <li key={subject.subject}>\n <strong>{subject.subject}</strong>{' '}\n <span className=\"va-note\">\n {briefly(subject)} moved\n </span>\n {index < ASKED ? <Journey client={client} subject={subject.subject} /> : null}\n </li>\n ))}\n </ul>\n {crossing.unreachedMoved.length <= ASKED ? null : (\n <p className=\"va-note\">\n The record was read for the first {number(ASKED)}. Open the rest from the rail.\n </p>\n )}\n </section>\n )}\n\n {crossing.reachedStill.length === 0 ? null : (\n <section className=\"va-band va-inert\">\n <h3>Reached, and did not move</h3>\n <p className=\"va-note\">\n The commit arrives at these and the render is identical to the baseline. Nothing else on\n this page says anything at all about a green subject.\n </p>\n <ul className=\"va-reach-list va-columns\">\n {crossing.reachedStill.map((subject) => (\n <li key={subject.subject}>\n <strong>{subject.subject}</strong>{' '}\n <Via through={reach.subjects?.[subject.subject]?.through ?? []} />\n </li>\n ))}\n </ul>\n </section>\n )}\n\n <Trails reach={reach} />\n <Unplaced crossing={crossing} />\n </>\n );\n}\n\nfunction Cell({\n tone,\n n,\n title,\n note,\n}: {\n readonly tone: string;\n readonly n: number;\n readonly title: string;\n readonly note: string;\n}): ReactElement {\n // A zero is drawn quietly rather than hidden. An omitted quadrant reads as a\n // question that was not asked; a zero is the answer to it.\n return (\n <div className={n === 0 ? `va-cell ${tone} va-zero` : `va-cell ${tone}`}>\n <span className=\"va-cell-n va-num\">{number(n)}</span>\n <span className=\"va-cell-title\">{title}</span>\n <span className=\"va-cell-note\">{note}</span>\n </div>\n );\n}\n\n/**\n * The chains, which are the part a reviewer can check.\n *\n * A component named without its chain is an assertion. `tokens.css →\n * button.tsx → Button` is a claim somebody can open three files and disprove,\n * which is the only kind worth putting on a review surface.\n */\nfunction Trails({ reach }: { readonly reach: ReachView }): ReactElement | null {\n if (reach.components.length === 0) return null;\n const shown = reach.components.slice(0, TRAILS);\n\n return (\n <section className=\"va-band\">\n <h3>How the diff arrives</h3>\n <ul className=\"va-trails\">\n {shown.map((entry) => (\n <li key={entry.component}>\n {entry.trail.map((step, index) => (\n <span key={step}>\n {index === 0 ? null : <span className=\"va-arrow\">→</span>}\n {index === entry.trail.length - 1 ? <strong>{step}</strong> : <code>{step}</code>}\n </span>\n ))}\n {entry.throughUnread === undefined ? null : (\n <span className=\"va-axis va-unread\">begins at a file the scan could not read</span>\n )}\n </li>\n ))}\n </ul>\n {reach.components.length <= shown.length ? null : (\n <p className=\"va-note\">\n {count(reach.components.length - shown.length, 'further component')} reached, not listed.\n </p>\n )}\n </section>\n );\n}\n\n/**\n * The subjects the crossing could not seat, said rather than absorbed.\n *\n * Two different silences, kept as two. A subject whose baseline carries no\n * component list is one this surface cannot answer for; a new or incomparable\n * subject is one where the question does not apply. Printing a single number for\n * both would invite the reader to fix the wrong thing.\n */\nfunction Unplaced({ crossing }: { readonly crossing: Crossing }): ReactElement | null {\n if (crossing.unplaced + crossing.incomparable === 0) return null;\n\n return (\n <p className=\"va-note va-reach-note\">\n {crossing.unplaced === 0 ? null : (\n <>\n {count(crossing.unplaced, 'subject')} could not be placed on either axis, because{' '}\n {crossing.unplaced === 1 ? 'its baseline records' : 'their baselines record'} no component\n list.{' '}\n </>\n )}\n {crossing.incomparable === 0 ? null : (\n <>\n {count(crossing.incomparable, 'subject')}{' '}\n {crossing.incomparable === 1 ? 'was' : 'were'} new or could not be compared, so there is\n no moved-or-still to cross.\n </>\n )}\n </p>\n );\n}\n\n/**\n * Where the attribution is knowingly wider than the diff.\n *\n * Both lists name their files rather than counting them, because both are things\n * an operator can go and fix, and a bare count tells them there is nothing to do.\n */\nfunction Holes({ reach }: { readonly reach: ReachView }): ReactElement | null {\n if (reach.unscanned === undefined && reach.opaque === undefined) return null;\n\n return (\n <p className=\"va-note va-reach-note\">\n {reach.unscanned === undefined ? null : (\n <>\n {count(reach.unscanned.length, 'changed file')} under the scanned roots{' '}\n {reach.unscanned.length === 1 ? 'is' : 'are'} not in the graph:{' '}\n <Files names={reach.unscanned} />.{' '}\n </>\n )}\n {reach.opaque === undefined ? null : (\n <>\n {count(reach.opaque.length, 'file')} was walked as changed because its own imports could\n not be read: <Files names={reach.opaque.map((hole) => hole.file)} />.\n </>\n )}\n </p>\n );\n}\n\n/**\n * Which of a subject's own components the commit arrives at.\n *\n * `SubjectReach.through` holds component names, not paths, and they are set as\n * prose for that reason: a reader shown `MainNav` in the same monospace face as\n * `app/src/components/MainNav.tsx` reads the two as the same kind of thing and\n * starts looking for a file by that name.\n *\n * Nothing at all when the list is empty, rather than a dangling `via`. Empty here\n * is `reached: false`, which this list is not.\n */\nfunction Via({ through }: { readonly through: readonly string[] }): ReactElement | null {\n if (through.length === 0) return null;\n\n return <span className=\"va-note\">via {through.join(', ')}</span>;\n}\n\nfunction Files({ names }: { readonly names: readonly string[] }): ReactElement {\n return (\n <>\n {names.map((name, index) => (\n <span key={name}>\n {index === 0 ? null : ', '}\n <code>{name}</code>\n </span>\n ))}\n </>\n );\n}\n\n/**\n * What the record says about a subject nothing in this commit reaches.\n *\n * Read on mount rather than behind a button, which is the opposite of the\n * per-subject history elsewhere on this page and is deliberate. A reviewer who\n * has opened a subject has already decided it is worth their time; this list is\n * short by construction and every row on it is *already* the anomaly. \"Has this\n * moved on its own before\" is the entire reason the row is here, so waiting to be\n * asked it would be waiting to be asked the point.\n */\nfunction Journey({\n client,\n subject,\n}: {\n readonly client: ReviewClient;\n readonly subject: string;\n}): ReactElement {\n const [answer, setAnswer] = useState<\n | { readonly state: 'reading' }\n | { readonly state: 'failed'; readonly why: string }\n | { readonly state: 'read'; readonly flakiness: Flakiness }\n >({ state: 'reading' });\n\n const load = useCallback(async (): Promise<void> => {\n try {\n setAnswer({ state: 'read', flakiness: await client.flakiness(subject) });\n } catch (error) {\n setAnswer({ state: 'failed', why: error instanceof Error ? error.message : String(error) });\n }\n }, [client, subject]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n if (answer.state === 'reading') return <p className=\"va-note\">Reading the record…</p>;\n\n // A record that could not be read says so. Blank here reads as \"nothing on\n // file\", which is the one answer that would send a reviewer to approve an\n // otherwise unexplained change.\n if (answer.state === 'failed') {\n return <p className=\"va-note va-lost\">The record could not be read: {answer.why}</p>;\n }\n\n return <StabilityLine flakiness={answer.flakiness} />;\n}\n"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import type { SubjectView } from '../review.js';
|
|
3
|
+
/**
|
|
4
|
+
* The boxes, and the table that names them.
|
|
5
|
+
*
|
|
6
|
+
* Apart from [`viewer.tsx`](./viewer.tsx) because the stage is one question — how
|
|
7
|
+
* do I look at two images — and this is the other: *which of these boxes is the
|
|
8
|
+
* one somebody edited, and who owns it*. The two are drawn together and read
|
|
9
|
+
* together, and a reviewer moves between them constantly, which is why the focus
|
|
10
|
+
* is one index shared by both rather than a hover state in each.
|
|
11
|
+
*
|
|
12
|
+
* The row and the rectangle are the same finding twice. Pointing at either lights
|
|
13
|
+
* the other, because a rectangle 700 pixels down a route capture and a row in a
|
|
14
|
+
* table are otherwise two lists a reader has to join by counting.
|
|
15
|
+
*/
|
|
16
|
+
export interface RegionFocus {
|
|
17
|
+
/** Which region is lit, by report order. `null` is none. */
|
|
18
|
+
readonly focus: number | null;
|
|
19
|
+
readonly onFocus: (index: number | null) => void;
|
|
20
|
+
/** Bring this region into view on the stage. */
|
|
21
|
+
readonly onJump: (index: number) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Region rectangles over the candidate, cause and collateral drawn apart.
|
|
25
|
+
*
|
|
26
|
+
* Positioned in percentages of the raster's own dimensions, which the build
|
|
27
|
+
* carries, rather than of the rendered element. Measuring the image in the
|
|
28
|
+
* browser would place every box correctly only after it had loaded, and wrongly
|
|
29
|
+
* for the frame before that — and a box in the wrong place is worse than no box,
|
|
30
|
+
* because it attributes a change to whatever it happens to land on.
|
|
31
|
+
*
|
|
32
|
+
* Percentages are also what survives the zoom: the stage scales the plate and
|
|
33
|
+
* every box scales with it, so nothing here has to know the magnification.
|
|
34
|
+
*
|
|
35
|
+
* Only the causes and whatever is under attention are named on the picture. Thirty
|
|
36
|
+
* collateral captions over a page fitted to a pane overlap into a grey field, and
|
|
37
|
+
* a reviewer who cannot read any of them is worse off than one reading two — so
|
|
38
|
+
* the rest carry their ordinal, which is the number they are listed under, and the
|
|
39
|
+
* sentence arrives on hover and in the table. Nothing is dropped: every region
|
|
40
|
+
* that was found is a box and a row, both of them always.
|
|
41
|
+
*/
|
|
42
|
+
export declare function RegionOverlay({ subject, box, focus, onFocus, }: {
|
|
43
|
+
readonly subject: SubjectView;
|
|
44
|
+
/**
|
|
45
|
+
* The frame the percentages are of, when it is not the candidate's own.
|
|
46
|
+
*
|
|
47
|
+
* Region coordinates are in the comparison's space, and a comparison pads to
|
|
48
|
+
* the union of the two captures — so on a subject whose baseline was a
|
|
49
|
+
* different size, the candidate's dimensions are the wrong denominator and
|
|
50
|
+
* every box lands short by the difference.
|
|
51
|
+
*/
|
|
52
|
+
readonly box?: {
|
|
53
|
+
readonly width: number;
|
|
54
|
+
readonly height: number;
|
|
55
|
+
} | undefined;
|
|
56
|
+
readonly focus?: number | null;
|
|
57
|
+
readonly onFocus?: ((index: number | null) => void) | undefined;
|
|
58
|
+
}): ReactElement | null;
|
|
59
|
+
/**
|
|
60
|
+
* The same regions as rows, which is the half a picture cannot deliver.
|
|
61
|
+
*
|
|
62
|
+
* A rectangle says *here*. It cannot say `src/ui/Button.tsx:18`, and the file is
|
|
63
|
+
* the column that lets a reviewer hand the change to whoever owns it rather than
|
|
64
|
+
* deciding it themselves. Ordered as the report ordered them — causes first,
|
|
65
|
+
* never by area, which reports the container that reflowed instead of the
|
|
66
|
+
* component that was edited.
|
|
67
|
+
*/
|
|
68
|
+
export declare function RegionTable({ subject, sourced, focus, onFocus, onJump, }: {
|
|
69
|
+
readonly subject: SubjectView;
|
|
70
|
+
/**
|
|
71
|
+
* Whether the *run* resolved any source file at all.
|
|
72
|
+
*
|
|
73
|
+
* A property of the run's source index, not of one capture, and the caller who
|
|
74
|
+
* holds the build is the only one who can say it. Left out, this falls back to
|
|
75
|
+
* what one render can see on its own, which is strictly weaker: a capture whose
|
|
76
|
+
* every region belongs to a dependency resolves nothing and would report a run
|
|
77
|
+
* with no index — the same name then reads two ways on two surfaces, and a
|
|
78
|
+
* reviewer takes one of them for a bug.
|
|
79
|
+
*/
|
|
80
|
+
readonly sourced?: boolean | undefined;
|
|
81
|
+
} & RegionFocus): ReactElement | null;
|
|
82
|
+
//# sourceMappingURL=regions.d.ts.map
|