@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,125 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { loudestFirst, senses } from './sense.js';
|
|
3
|
+
import { Go } from './shell.js';
|
|
4
|
+
import { count, number } from './text.js';
|
|
5
|
+
const NONE = [];
|
|
6
|
+
export function consumersOf(build) {
|
|
7
|
+
const census = build.composition;
|
|
8
|
+
if (census === null)
|
|
9
|
+
return () => NONE;
|
|
10
|
+
const places = new Map(census.map((each) => [each.component, each]));
|
|
11
|
+
const subjects = new Map(build.subjects.map((each) => [each.subject, each]));
|
|
12
|
+
return (component) => {
|
|
13
|
+
const place = places.get(component);
|
|
14
|
+
if (place === undefined)
|
|
15
|
+
return NONE;
|
|
16
|
+
const drawn = new Set(place.subjects);
|
|
17
|
+
const found = place.within.flatMap((name) => read(name, places.get(name), drawn, subjects));
|
|
18
|
+
return [...found].sort(worstFirst);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function read(component, place, drawn, subjects) {
|
|
22
|
+
if (place === undefined)
|
|
23
|
+
return [];
|
|
24
|
+
const bands = new Set();
|
|
25
|
+
let renders = 0;
|
|
26
|
+
let still = 0;
|
|
27
|
+
let pushed = 0;
|
|
28
|
+
let moved = 0;
|
|
29
|
+
let unread = 0;
|
|
30
|
+
for (const name of place.subjects) {
|
|
31
|
+
if (!drawn.has(name))
|
|
32
|
+
continue;
|
|
33
|
+
const subject = subjects.get(name);
|
|
34
|
+
if (subject === undefined)
|
|
35
|
+
continue;
|
|
36
|
+
if (subject.moved === undefined) {
|
|
37
|
+
unread += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
renders += 1;
|
|
41
|
+
const entry = subject.moved.find((each) => each.component === component);
|
|
42
|
+
if (entry === undefined)
|
|
43
|
+
still += 1;
|
|
44
|
+
else if (!entry.cause)
|
|
45
|
+
pushed += 1;
|
|
46
|
+
else {
|
|
47
|
+
moved += 1;
|
|
48
|
+
for (const band of entry.bands)
|
|
49
|
+
bands.add(band);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (renders === 0 && unread === 0)
|
|
53
|
+
return [];
|
|
54
|
+
return [{ component, renders, still, pushed, moved, bands: loudestFirst(bands), unread }];
|
|
55
|
+
}
|
|
56
|
+
/** Moved on its own, then only-shifted, then unchanged. */
|
|
57
|
+
function worstFirst(left, right) {
|
|
58
|
+
const rank = (each) => (each.moved > 0 ? 0 : each.pushed > 0 ? 1 : 2);
|
|
59
|
+
return rank(left) - rank(right) || left.component.localeCompare(right.component);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* How many names a line will carry before it stops being a line a reviewer reads.
|
|
63
|
+
*
|
|
64
|
+
* A button in three thousand shots has more consumers than fit anywhere, and the
|
|
65
|
+
* ones being elided here are the two states nobody needs a name for. What is
|
|
66
|
+
* never elided is the pile above: a consumer that moved on its own gets a row of
|
|
67
|
+
* its own however many of them there are, because that pile is the review.
|
|
68
|
+
*/
|
|
69
|
+
const NAMES = 8;
|
|
70
|
+
/**
|
|
71
|
+
* What the things drawing this one did — the exceptions named, the rest counted.
|
|
72
|
+
*
|
|
73
|
+
* The first draft gave every consumer a row and led with *2 held still, 2 pushed,
|
|
74
|
+
* 1 moved on its own*, which is three numbers and no names: the reviewer's
|
|
75
|
+
* question is **which**, and a count answers it only if they then read five rows
|
|
76
|
+
* and sort them back into three piles themselves. It also spent a row each on
|
|
77
|
+
* consumers that did nothing, which is the pile with the least to say and, at any
|
|
78
|
+
* real scale, the largest.
|
|
79
|
+
*
|
|
80
|
+
* So the shape is inverted. A consumer that moved in a way this change does not
|
|
81
|
+
* explain gets a row, its bands and its render count — that is the review. The
|
|
82
|
+
* ones that only shifted and the ones that held still are named on one line each
|
|
83
|
+
* and nothing more, because *`MainNav` was drawn eight times and is unchanged* is
|
|
84
|
+
* a fact worth having and not a fact worth eight lines of screen.
|
|
85
|
+
*/
|
|
86
|
+
export function Consumers({ found, build, changes, go, }) {
|
|
87
|
+
if (found.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
const moved = found.filter((each) => each.moved > 0);
|
|
90
|
+
const shifted = found.filter((each) => each.moved === 0 && each.pushed > 0);
|
|
91
|
+
const still = found.filter((each) => each.moved === 0 && each.pushed === 0 && each.renders > 0);
|
|
92
|
+
const unread = found.filter((each) => each.renders === 0);
|
|
93
|
+
return (_jsxs("section", { className: "va-consumers", children: [_jsx("h2", { children: "Consumers" }), _jsx("p", { className: "va-consumers-tally", children: moved.length === 0 ? (_jsxs("strong", { children: ["Nothing of the ", count(found.length, 'component'), " that draw it moved on its own."] })) : (_jsxs("strong", { className: "va-consumers-own", children: [names(moved), " moved in ", moved.length === 1 ? 'a way' : 'ways', " this change does not explain."] })) }), moved.length === 0 ? null : (_jsx("ul", { className: "va-consumers-list", children: moved.map((each) => (_jsxs("li", { className: "va-consumer-moved", children: [_jsx(Name, { each: each, build: build, changes: changes, go: go }), _jsxs("span", { className: "va-note", children: [_jsx("em", { children: senses(each.bands) }), " \u2014 ", number(each.moved), " of ", number(each.renders), each.unread === 0 ? null : _jsxs(_Fragment, { children: [" \u00B7 ", number(each.unread), " not read"] })] })] }, each.component))) })), _jsxs("p", { className: "va-consumers-rest", children: [shifted.length === 0 ? null : (_jsxs("span", { children: ["Only their box moved: ", _jsx(Named, { of: shifted }), ".", ' '] })), still.length === 0 ? null : (_jsxs("span", { children: ["Unchanged: ", _jsx(Named, { of: still }), ".", ' '] })), unread.length === 0 ? null : (_jsxs("span", { className: "va-alarm-text", children: ["Nothing compared hashes for ", _jsx(Named, { of: unread, unread: false }), ", so no render says what ", unread.length === 1 ? 'it' : 'they', " did."] }))] })] }));
|
|
94
|
+
}
|
|
95
|
+
function Name({ each, build, changes, go, }) {
|
|
96
|
+
if (!changes.has(each.component)) {
|
|
97
|
+
return _jsx("span", { className: "va-consumer-name", children: each.component });
|
|
98
|
+
}
|
|
99
|
+
return (_jsx(Go, { to: { page: 'change', build, change: each.component }, go: go, className: "va-consumer-name", title: `${each.component} is a change on this build, with a decision of its own`, children: each.component }));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The names, capped, with the remainder said rather than dropped — and with the
|
|
103
|
+
* renders nobody read carried on the name they belong to.
|
|
104
|
+
*
|
|
105
|
+
* *Unchanged* over a consumer with two renders read and one not is true of the
|
|
106
|
+
* two and says nothing about the third, and a reader has no way to tell that
|
|
107
|
+
* from a clean three. The count goes on the name because that is the only place
|
|
108
|
+
* it stays attached once the pile is a line.
|
|
109
|
+
*/
|
|
110
|
+
function Named({ of, unread = true, }) {
|
|
111
|
+
const shown = of.slice(0, NAMES);
|
|
112
|
+
const rest = of.length - shown.length;
|
|
113
|
+
return (_jsxs("span", { title: of.map((each) => each.component).join(', '), children: [shown.map((each, index) => (_jsxs("span", { children: [index === 0 ? '' : ', ', each.component, !unread || each.unread === 0 ? null : ` (${number(each.unread)} not read)`] }, each.component))), rest === 0 ? null : ` and ${number(rest)} more`] }));
|
|
114
|
+
}
|
|
115
|
+
/** The same, in a sentence that starts with them. */
|
|
116
|
+
function names(of) {
|
|
117
|
+
const shown = of.slice(0, NAMES).map((each) => each.component);
|
|
118
|
+
const rest = of.length - shown.length;
|
|
119
|
+
if (rest > 0)
|
|
120
|
+
return `${shown.join(', ')} and ${number(rest)} more`;
|
|
121
|
+
if (shown.length === 1)
|
|
122
|
+
return shown[0] ?? '';
|
|
123
|
+
return `${shown.slice(0, -1).join(', ')} and ${shown[shown.length - 1] ?? ''}`;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=consumers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consumers.js","sourceRoot":"","sources":["../../src/ui/consumers.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * What happened to the things that draw this one.\n *\n * A reviewer who edited `ui/button.tsx` does not need to be told the button\n * changed. They wrote it. The question the screenshot exists to answer is the\n * other one — *and does everything that uses it still look right* — and the page\n * answered it by listing every component that moved anywhere near, alphabetised,\n * with no way to tell a card that reflowed from a nav that did something of its\n * own.\n *\n * The census names the consumers directly: `within` is what encloses this\n * component, per render, over the suite. Held to the renders where both are\n * drawn, that turns into one row per consumer and a state for each.\n *\n * ## Four states, and `cause` is what separates the middle two\n *\n * A component hash carries `cause` beside its bands — *its own content moved*,\n * as against *only its rect did*. That distinction is the whole answer here. A\n * card whose row grew because the button inside it got taller was **pushed**: it\n * is behaving correctly, it is the consequence a reviewer is checking for, and\n * printing it as a change would put five components on the alarm list for one\n * edit. A card that moved on its own did something the button does not explain.\n *\n * So: **held still** (no entry at all), **pushed** (an entry, `cause` false),\n * **moved** (an entry, `cause` true, and the bands say in what), and **not read**\n * — a render whose baseline carried no hashes, which is not a render where\n * anything held still.\n *\n * ## What it will not say\n *\n * That a consumer nobody rendered is fine. It says nothing about it, because\n * nothing looked: the rows here are renders, and a consumer with no story and no\n * route has none. The import graph knows those names and this does not read it.\n */\n\nimport type { ReactElement } from 'react';\nimport type { BuildDetail, Placement, SubjectView } from '../review-types.js';\nimport type { Route } from './route.js';\nimport { loudestFirst, senses } from './sense.js';\nimport { Go } from './shell.js';\nimport { count, number } from './text.js';\n\n/** What one component that draws this one did, over the renders they share. */\nexport interface Consumer {\n readonly component: string;\n /** Renders drawing both, where the hashes were read. */\n readonly renders: number;\n /** Of those: no entry at all — nothing of it differs. */\n readonly still: number;\n /** Of those: only its rect moved. The consequence, behaving as it should. */\n readonly pushed: number;\n /** Of those: its own content moved, which this change does not explain. */\n readonly moved: number;\n /** The bands it moved in on its own, loudest first. Empty unless `moved`. */\n readonly bands: readonly string[];\n /** Renders drawing both where nothing compared hashes. */\n readonly unread: number;\n}\n\n/** One build's answer to *what did the things that draw this do*. */\nexport type Consuming = (component: string) => readonly Consumer[];\n\nconst NONE: readonly Consumer[] = [];\n\nexport function consumersOf(build: BuildDetail): Consuming {\n const census = build.composition;\n if (census === null) return () => NONE;\n\n const places = new Map(census.map((each) => [each.component, each]));\n const subjects = new Map(build.subjects.map((each) => [each.subject, each]));\n\n return (component) => {\n const place = places.get(component);\n if (place === undefined) return NONE;\n\n const drawn = new Set(place.subjects);\n const found = place.within.flatMap((name) => read(name, places.get(name), drawn, subjects));\n return [...found].sort(worstFirst);\n };\n}\n\nfunction read(\n component: string,\n place: Placement | undefined,\n drawn: ReadonlySet<string>,\n subjects: ReadonlyMap<string, SubjectView>,\n): readonly Consumer[] {\n if (place === undefined) return [];\n\n const bands = new Set<string>();\n let renders = 0;\n let still = 0;\n let pushed = 0;\n let moved = 0;\n let unread = 0;\n\n for (const name of place.subjects) {\n if (!drawn.has(name)) continue;\n const subject = subjects.get(name);\n if (subject === undefined) continue;\n if (subject.moved === undefined) {\n unread += 1;\n continue;\n }\n\n renders += 1;\n const entry = subject.moved.find((each) => each.component === component);\n if (entry === undefined) still += 1;\n else if (!entry.cause) pushed += 1;\n else {\n moved += 1;\n for (const band of entry.bands) bands.add(band);\n }\n }\n\n if (renders === 0 && unread === 0) return [];\n return [{ component, renders, still, pushed, moved, bands: loudestFirst(bands), unread }];\n}\n\n/** Moved on its own, then only-shifted, then unchanged. */\nfunction worstFirst(left: Consumer, right: Consumer): number {\n const rank = (each: Consumer): number => (each.moved > 0 ? 0 : each.pushed > 0 ? 1 : 2);\n return rank(left) - rank(right) || left.component.localeCompare(right.component);\n}\n\n/**\n * How many names a line will carry before it stops being a line a reviewer reads.\n *\n * A button in three thousand shots has more consumers than fit anywhere, and the\n * ones being elided here are the two states nobody needs a name for. What is\n * never elided is the pile above: a consumer that moved on its own gets a row of\n * its own however many of them there are, because that pile is the review.\n */\nconst NAMES = 8;\n\n/**\n * What the things drawing this one did — the exceptions named, the rest counted.\n *\n * The first draft gave every consumer a row and led with *2 held still, 2 pushed,\n * 1 moved on its own*, which is three numbers and no names: the reviewer's\n * question is **which**, and a count answers it only if they then read five rows\n * and sort them back into three piles themselves. It also spent a row each on\n * consumers that did nothing, which is the pile with the least to say and, at any\n * real scale, the largest.\n *\n * So the shape is inverted. A consumer that moved in a way this change does not\n * explain gets a row, its bands and its render count — that is the review. The\n * ones that only shifted and the ones that held still are named on one line each\n * and nothing more, because *`MainNav` was drawn eight times and is unchanged* is\n * a fact worth having and not a fact worth eight lines of screen.\n */\nexport function Consumers({\n found,\n build,\n changes,\n go,\n}: {\n readonly found: readonly Consumer[];\n readonly build: string;\n /** Components with a change page of their own, so a link goes somewhere. */\n readonly changes: ReadonlySet<string>;\n readonly go: (route: Route) => void;\n}): ReactElement | null {\n if (found.length === 0) return null;\n\n const moved = found.filter((each) => each.moved > 0);\n const shifted = found.filter((each) => each.moved === 0 && each.pushed > 0);\n const still = found.filter((each) => each.moved === 0 && each.pushed === 0 && each.renders > 0);\n const unread = found.filter((each) => each.renders === 0);\n\n return (\n <section className=\"va-consumers\">\n <h2>Consumers</h2>\n\n <p className=\"va-consumers-tally\">\n {moved.length === 0 ? (\n <strong>\n Nothing of the {count(found.length, 'component')} that draw it moved on its own.\n </strong>\n ) : (\n <strong className=\"va-consumers-own\">\n {names(moved)} moved in {moved.length === 1 ? 'a way' : 'ways'} this change does not\n explain.\n </strong>\n )}\n </p>\n\n {moved.length === 0 ? null : (\n <ul className=\"va-consumers-list\">\n {moved.map((each) => (\n <li key={each.component} className=\"va-consumer-moved\">\n <Name each={each} build={build} changes={changes} go={go} />\n <span className=\"va-note\">\n <em>{senses(each.bands)}</em> — {number(each.moved)} of {number(each.renders)}\n {each.unread === 0 ? null : <> · {number(each.unread)} not read</>}\n </span>\n </li>\n ))}\n </ul>\n )}\n\n {/* The other three piles, one line each. Named, because *2 pushed* sends a\n reviewer looking for which two, and counted rather than listed because\n at scale these are the hundreds. */}\n <p className=\"va-consumers-rest\">\n {shifted.length === 0 ? null : (\n <span>\n Only their box moved: <Named of={shifted} />.{' '}\n </span>\n )}\n {still.length === 0 ? null : (\n <span>\n Unchanged: <Named of={still} />.{' '}\n </span>\n )}\n {unread.length === 0 ? null : (\n <span className=\"va-alarm-text\">\n Nothing compared hashes for <Named of={unread} unread={false} />, so no render says\n what {unread.length === 1 ? 'it' : 'they'} did.\n </span>\n )}\n </p>\n </section>\n );\n}\n\nfunction Name({\n each,\n build,\n changes,\n go,\n}: {\n readonly each: Consumer;\n readonly build: string;\n readonly changes: ReadonlySet<string>;\n readonly go: (route: Route) => void;\n}): ReactElement {\n if (!changes.has(each.component)) {\n return <span className=\"va-consumer-name\">{each.component}</span>;\n }\n return (\n <Go\n to={{ page: 'change', build, change: each.component }}\n go={go}\n className=\"va-consumer-name\"\n title={`${each.component} is a change on this build, with a decision of its own`}\n >\n {each.component}\n </Go>\n );\n}\n\n/**\n * The names, capped, with the remainder said rather than dropped — and with the\n * renders nobody read carried on the name they belong to.\n *\n * *Unchanged* over a consumer with two renders read and one not is true of the\n * two and says nothing about the third, and a reader has no way to tell that\n * from a clean three. The count goes on the name because that is the only place\n * it stays attached once the pile is a line.\n */\nfunction Named({\n of,\n unread = true,\n}: {\n readonly of: readonly Consumer[];\n /** Off where the sentence around it already says nothing was read. */\n readonly unread?: boolean;\n}): ReactElement {\n const shown = of.slice(0, NAMES);\n const rest = of.length - shown.length;\n return (\n <span title={of.map((each) => each.component).join(', ')}>\n {shown.map((each, index) => (\n <span key={each.component}>\n {index === 0 ? '' : ', '}\n {each.component}\n {!unread || each.unread === 0 ? null : ` (${number(each.unread)} not read)`}\n </span>\n ))}\n {rest === 0 ? null : ` and ${number(rest)} more`}\n </span>\n );\n}\n\n/** The same, in a sentence that starts with them. */\nfunction names(of: readonly Consumer[]): string {\n const shown = of.slice(0, NAMES).map((each) => each.component);\n const rest = of.length - shown.length;\n if (rest > 0) return `${shown.join(', ')} and ${number(rest)} more`;\n if (shown.length === 1) return shown[0] ?? '';\n return `${shown.slice(0, -1).join(', ')} and ${shown[shown.length - 1] ?? ''}`;\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where this component did not move — the arm no diffing tool has.
|
|
3
|
+
*
|
|
4
|
+
* Every other line on this page is about the renders that changed, which is the
|
|
5
|
+
* only population a pixel comparison can see. The run holds a second one: sites
|
|
6
|
+
* of the same component, under the *same props*, whose hashes it read and found
|
|
7
|
+
* unmoved. That is a control group, and it turns a change from a list of
|
|
8
|
+
* casualties into a measurement.
|
|
9
|
+
*
|
|
10
|
+
* The reading is what the reviewer wants and it is short. `Button` was edited and
|
|
11
|
+
* held in four renders means the edit was to something those four do not use — a
|
|
12
|
+
* variant, a size, a state — and it bounds the blast radius with evidence rather
|
|
13
|
+
* than with a count of what happened to be collected. `Button` was edited and
|
|
14
|
+
* held nowhere means nothing under those props escaped it.
|
|
15
|
+
*
|
|
16
|
+
* ## Why this can only be the run's answer
|
|
17
|
+
*
|
|
18
|
+
* The props class is the load-bearing half and it never crosses the wire. Two
|
|
19
|
+
* renders of a component given different inputs are not controls for each other —
|
|
20
|
+
* they are two questions that share a name — and the store carries the census
|
|
21
|
+
* without the digest each rendering was grouped under. The page can see *where a
|
|
22
|
+
* component appears*; only the run can see *where it appears the same way*.
|
|
23
|
+
*
|
|
24
|
+
* ## Empty is two findings, and the denominator is what tells them apart
|
|
25
|
+
*
|
|
26
|
+
* A control group of nothing is either *this component renders nowhere else with
|
|
27
|
+
* these inputs* — a suite with nothing to say — or *it renders in four other
|
|
28
|
+
* places and moved in all four*, which is the suite saying the loudest thing it
|
|
29
|
+
* can. On the example every control group is the second kind: the restyle reached
|
|
30
|
+
* every render of `Button` and `CardFooter` under their own props, and a page
|
|
31
|
+
* that stayed silent about that would drop the one measurement it had.
|
|
32
|
+
*
|
|
33
|
+
* So `compared` is read beside `held`, and where it is absent — a build ingested
|
|
34
|
+
* before the run carried it — the section says nothing at all. A missing input is
|
|
35
|
+
* not a finding.
|
|
36
|
+
*/
|
|
37
|
+
import type { ReactElement } from 'react';
|
|
38
|
+
import type { Origin } from './grouping.js';
|
|
39
|
+
import type { Route } from './route.js';
|
|
40
|
+
/** Renders that held, the renders they are a control for, and the pool. */
|
|
41
|
+
export interface Controls {
|
|
42
|
+
/** Subjects where the same component with the same props was read and unmoved. */
|
|
43
|
+
readonly held: readonly string[];
|
|
44
|
+
/** Renders of this change that had a control, out of every render it has. */
|
|
45
|
+
readonly of: number;
|
|
46
|
+
readonly renders: number;
|
|
47
|
+
/**
|
|
48
|
+
* The widest pool any appearance was compared against, or `undefined` when no
|
|
49
|
+
* appearance said.
|
|
50
|
+
*
|
|
51
|
+
* The widest and not the sum: the pools overlap — the same four other renders
|
|
52
|
+
* of `Button` are the comparison for each of its appearances — and adding them
|
|
53
|
+
* would report sixteen comparisons where four were made.
|
|
54
|
+
*/
|
|
55
|
+
readonly compared?: number;
|
|
56
|
+
}
|
|
57
|
+
export declare function controlsFor(origin: Origin): Controls;
|
|
58
|
+
export declare function HeldStill({ controls, build, go, }: {
|
|
59
|
+
readonly controls: Controls;
|
|
60
|
+
readonly build: string;
|
|
61
|
+
readonly go: (route: Route) => void;
|
|
62
|
+
}): ReactElement | null;
|
|
63
|
+
//# sourceMappingURL=control.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Go } from './shell.js';
|
|
3
|
+
import { count } from './text.js';
|
|
4
|
+
export function controlsFor(origin) {
|
|
5
|
+
const held = [];
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
let of = 0;
|
|
8
|
+
let compared;
|
|
9
|
+
for (const { movement } of origin.appearances) {
|
|
10
|
+
// Absent is not empty: a build ingested before attributions were carried has
|
|
11
|
+
// no movement at all, and counting it as a render without a control would
|
|
12
|
+
// report a missing input as a finding.
|
|
13
|
+
if (movement === undefined)
|
|
14
|
+
continue;
|
|
15
|
+
if (movement.held.length > 0)
|
|
16
|
+
of += 1;
|
|
17
|
+
if (movement.compared !== undefined && (compared === undefined || movement.compared > compared)) {
|
|
18
|
+
compared = movement.compared;
|
|
19
|
+
}
|
|
20
|
+
for (const subject of movement.held) {
|
|
21
|
+
if (seen.has(subject))
|
|
22
|
+
continue;
|
|
23
|
+
seen.add(subject);
|
|
24
|
+
held.push(subject);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { held, of, renders: origin.appearances.length, ...(compared === undefined ? {} : { compared }) };
|
|
28
|
+
}
|
|
29
|
+
export function HeldStill({ controls, build, go, }) {
|
|
30
|
+
if (controls.held.length === 0) {
|
|
31
|
+
// Nothing held, and the two reasons for that point opposite ways. Only the
|
|
32
|
+
// denominator separates them, and without one there is nothing to say.
|
|
33
|
+
if (controls.compared === undefined || controls.compared === 0)
|
|
34
|
+
return null;
|
|
35
|
+
return (_jsxs("p", { className: "va-held va-held-none", children: [_jsx("span", { className: "va-held-mark", children: "Held nowhere" }), " \u2014 the same component under the same props was read in ", count(controls.compared, 'other render'), " and moved in every one. Nothing under these props escaped the change."] }));
|
|
36
|
+
}
|
|
37
|
+
return (_jsxs("p", { className: "va-held", children: [_jsx("span", { className: "va-held-mark", children: "Held still" }), " in ", count(controls.held.length, 'render'), " \u2014 same component, same props, hashes read and unmoved:", ' ', controls.held.map((subject, index) => (_jsxs("span", { children: [index === 0 ? null : ', ', _jsx(Go, { to: { page: 'subject', build, subject }, go: go, className: "va-held-go", children: subject })] }, subject))), ".", controls.of === controls.renders ? null : (_jsxs("span", { className: "va-note", children: [' ', "\u2014 a control for ", controls.of, " of ", controls.renders, " renders of this change."] }))] }));
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control.js","sourceRoot":"","sources":["../../src/ui/control.tsx"],"names":[],"mappings":";AAwCA,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAChC,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAoBlC,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,IAAI,QAA4B,CAAC;IAEjC,KAAK,MAAM,EAAE,QAAQ,EAAE,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QAC9C,6EAA6E;QAC7E,0EAA0E;QAC1E,uCAAuC;QACvC,IAAI,QAAQ,KAAK,SAAS;YAAE,SAAS;QACrC,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,EAAE,IAAI,CAAC,CAAC;QACtC,IAAI,QAAQ,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,EAAE,CAAC;YAChG,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC;QAC/B,CAAC;QACD,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;YACpC,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;gBAAE,SAAS;YAChC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAClB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;AAC3G,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,EACxB,QAAQ,EACR,KAAK,EACL,EAAE,GAKH;IACC,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,2EAA2E;QAC3E,uEAAuE;QACvE,IAAI,QAAQ,CAAC,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,QAAQ,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAE5E,OAAO,CACL,aAAG,SAAS,EAAC,sBAAsB,aACjC,eAAM,SAAS,EAAC,cAAc,6BAAoB,kEAChB,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC,8EAExE,CACL,CAAC;IACJ,CAAC;IAED,OAAO,CACL,aAAG,SAAS,EAAC,SAAS,aACpB,eAAM,SAAS,EAAC,cAAc,2BAAkB,UAAK,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,kEACrC,GAAG,EACvD,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,CACrC,2BACG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAC1B,KAAC,EAAE,IAAC,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,SAAS,EAAC,YAAY,YACxE,OAAO,GACL,KAJI,OAAO,CAKX,CACR,CAAC,OAED,QAAQ,CAAC,EAAE,KAAK,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACzC,gBAAM,SAAS,EAAC,SAAS,aACtB,GAAG,2BACa,QAAQ,CAAC,EAAE,UAAM,QAAQ,CAAC,OAAO,gCAC7C,CACR,IACC,CACL,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Where this component did not move — the arm no diffing tool has.\n *\n * Every other line on this page is about the renders that changed, which is the\n * only population a pixel comparison can see. The run holds a second one: sites\n * of the same component, under the *same props*, whose hashes it read and found\n * unmoved. That is a control group, and it turns a change from a list of\n * casualties into a measurement.\n *\n * The reading is what the reviewer wants and it is short. `Button` was edited and\n * held in four renders means the edit was to something those four do not use — a\n * variant, a size, a state — and it bounds the blast radius with evidence rather\n * than with a count of what happened to be collected. `Button` was edited and\n * held nowhere means nothing under those props escaped it.\n *\n * ## Why this can only be the run's answer\n *\n * The props class is the load-bearing half and it never crosses the wire. Two\n * renders of a component given different inputs are not controls for each other —\n * they are two questions that share a name — and the store carries the census\n * without the digest each rendering was grouped under. The page can see *where a\n * component appears*; only the run can see *where it appears the same way*.\n *\n * ## Empty is two findings, and the denominator is what tells them apart\n *\n * A control group of nothing is either *this component renders nowhere else with\n * these inputs* — a suite with nothing to say — or *it renders in four other\n * places and moved in all four*, which is the suite saying the loudest thing it\n * can. On the example every control group is the second kind: the restyle reached\n * every render of `Button` and `CardFooter` under their own props, and a page\n * that stayed silent about that would drop the one measurement it had.\n *\n * So `compared` is read beside `held`, and where it is absent — a build ingested\n * before the run carried it — the section says nothing at all. A missing input is\n * not a finding.\n */\n\nimport type { ReactElement } from 'react';\nimport type { Origin } from './grouping.js';\nimport type { Route } from './route.js';\nimport { Go } from './shell.js';\nimport { count } from './text.js';\n\n/** Renders that held, the renders they are a control for, and the pool. */\nexport interface Controls {\n /** Subjects where the same component with the same props was read and unmoved. */\n readonly held: readonly string[];\n /** Renders of this change that had a control, out of every render it has. */\n readonly of: number;\n readonly renders: number;\n /**\n * The widest pool any appearance was compared against, or `undefined` when no\n * appearance said.\n *\n * The widest and not the sum: the pools overlap — the same four other renders\n * of `Button` are the comparison for each of its appearances — and adding them\n * would report sixteen comparisons where four were made.\n */\n readonly compared?: number;\n}\n\nexport function controlsFor(origin: Origin): Controls {\n const held: string[] = [];\n const seen = new Set<string>();\n let of = 0;\n let compared: number | undefined;\n\n for (const { movement } of origin.appearances) {\n // Absent is not empty: a build ingested before attributions were carried has\n // no movement at all, and counting it as a render without a control would\n // report a missing input as a finding.\n if (movement === undefined) continue;\n if (movement.held.length > 0) of += 1;\n if (movement.compared !== undefined && (compared === undefined || movement.compared > compared)) {\n compared = movement.compared;\n }\n for (const subject of movement.held) {\n if (seen.has(subject)) continue;\n seen.add(subject);\n held.push(subject);\n }\n }\n\n return { held, of, renders: origin.appearances.length, ...(compared === undefined ? {} : { compared }) };\n}\n\nexport function HeldStill({\n controls,\n build,\n go,\n}: {\n readonly controls: Controls;\n readonly build: string;\n readonly go: (route: Route) => void;\n}): ReactElement | null {\n if (controls.held.length === 0) {\n // Nothing held, and the two reasons for that point opposite ways. Only the\n // denominator separates them, and without one there is nothing to say.\n if (controls.compared === undefined || controls.compared === 0) return null;\n\n return (\n <p className=\"va-held va-held-none\">\n <span className=\"va-held-mark\">Held nowhere</span> — the same component\n under the same props was read in {count(controls.compared, 'other render')} and moved in\n every one. Nothing under these props escaped the change.\n </p>\n );\n }\n\n return (\n <p className=\"va-held\">\n <span className=\"va-held-mark\">Held still</span> in {count(controls.held.length, 'render')} —\n same component, same props, hashes read and unmoved:{' '}\n {controls.held.map((subject, index) => (\n <span key={subject}>\n {index === 0 ? null : ', '}\n <Go to={{ page: 'subject', build, subject }} go={go} className=\"va-held-go\">\n {subject}\n </Go>\n </span>\n ))}\n .\n {controls.of === controls.renders ? null : (\n <span className=\"va-note\">\n {' '}\n — a control for {controls.of} of {controls.renders} renders of this change.\n </span>\n )}\n </p>\n );\n}\n"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The previous run, fetched once and read by whoever needs a row of it.
|
|
3
|
+
*
|
|
4
|
+
* The crossing used to belong to one panel, which fetched the earlier build for
|
|
5
|
+
* itself and rendered every row of it in one place at the bottom of the build
|
|
6
|
+
* page. Both halves of that were wrong. A reviewer deciding whether to approve a
|
|
7
|
+
* change in `Button` wants one sentence — *build 5 showed you this and nobody
|
|
8
|
+
* decided it* — beside the change, and they will not scroll nine thousand pixels
|
|
9
|
+
* to a section that also tells them about nineteen subjects they are not looking
|
|
10
|
+
* at. And the section itself is worth keeping whole, for the reader who came to
|
|
11
|
+
* ask what moved since Friday.
|
|
12
|
+
*
|
|
13
|
+
* So the fetch is a hook and the fold is [`shift.ts`](./shift.ts): one request,
|
|
14
|
+
* one crossing, and three readings that cannot disagree — the whole table on the
|
|
15
|
+
* run page, the rows under one change on its card, and the single row on a
|
|
16
|
+
* subject.
|
|
17
|
+
*/
|
|
18
|
+
import type { BuildDetail, BuildSummary } from '../review-types.js';
|
|
19
|
+
import type { ReviewClient } from './client.js';
|
|
20
|
+
import { type Divergence, type Shifted } from './shift.js';
|
|
21
|
+
/** The earlier run, or the reason there is nothing to place this one against. */
|
|
22
|
+
export type Crossing = {
|
|
23
|
+
readonly state: 'loading';
|
|
24
|
+
} | {
|
|
25
|
+
readonly state: 'none';
|
|
26
|
+
} | {
|
|
27
|
+
readonly state: 'failed';
|
|
28
|
+
readonly why: string;
|
|
29
|
+
} | {
|
|
30
|
+
readonly state: 'ready';
|
|
31
|
+
readonly earlier: BuildDetail;
|
|
32
|
+
readonly divergence: Divergence;
|
|
33
|
+
/** This subject's row, or `undefined` when neither run named it. */
|
|
34
|
+
readonly of: (subject: string) => Shifted | undefined;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* This build against the one before it.
|
|
38
|
+
*
|
|
39
|
+
* A failure is a state and never a silence. An empty crossing reads as *nothing
|
|
40
|
+
* has changed since the last run*, which is the sentence somebody merges on.
|
|
41
|
+
*/
|
|
42
|
+
export declare function useCrossing(client: ReviewClient, build: BuildDetail): Crossing;
|
|
43
|
+
/**
|
|
44
|
+
* The run before this one, as the store lists them.
|
|
45
|
+
*
|
|
46
|
+
* By position rather than by clock. Two runs pushed from one machine can carry
|
|
47
|
+
* the same timestamp to the millisecond — both example builds here do — and
|
|
48
|
+
* picking on `at` would compare a build against itself or against its own
|
|
49
|
+
* successor. The listing is newest first and breaks a tied clock by arrival, so
|
|
50
|
+
* position is a total order where the timestamp is not. The build it lands on is
|
|
51
|
+
* named on the page, because a reader who disagrees with the pick can only say so
|
|
52
|
+
* if they can see it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function previousOf(builds: readonly BuildSummary[], build: string): BuildSummary | undefined;
|
|
55
|
+
//# sourceMappingURL=crossing.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The previous run, fetched once and read by whoever needs a row of it.
|
|
3
|
+
*
|
|
4
|
+
* The crossing used to belong to one panel, which fetched the earlier build for
|
|
5
|
+
* itself and rendered every row of it in one place at the bottom of the build
|
|
6
|
+
* page. Both halves of that were wrong. A reviewer deciding whether to approve a
|
|
7
|
+
* change in `Button` wants one sentence — *build 5 showed you this and nobody
|
|
8
|
+
* decided it* — beside the change, and they will not scroll nine thousand pixels
|
|
9
|
+
* to a section that also tells them about nineteen subjects they are not looking
|
|
10
|
+
* at. And the section itself is worth keeping whole, for the reader who came to
|
|
11
|
+
* ask what moved since Friday.
|
|
12
|
+
*
|
|
13
|
+
* So the fetch is a hook and the fold is [`shift.ts`](./shift.ts): one request,
|
|
14
|
+
* one crossing, and three readings that cannot disagree — the whole table on the
|
|
15
|
+
* run page, the rows under one change on its card, and the single row on a
|
|
16
|
+
* subject.
|
|
17
|
+
*/
|
|
18
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
19
|
+
import { divergeFrom } from './shift.js';
|
|
20
|
+
/**
|
|
21
|
+
* This build against the one before it.
|
|
22
|
+
*
|
|
23
|
+
* A failure is a state and never a silence. An empty crossing reads as *nothing
|
|
24
|
+
* has changed since the last run*, which is the sentence somebody merges on.
|
|
25
|
+
*/
|
|
26
|
+
export function useCrossing(client, build) {
|
|
27
|
+
const [earlier, setEarlier] = useState({ state: 'loading' });
|
|
28
|
+
const id = build.build;
|
|
29
|
+
const load = useCallback(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const previous = previousOf(await client.builds(), id);
|
|
32
|
+
if (previous === undefined) {
|
|
33
|
+
setEarlier({ state: 'none' });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
setEarlier({ state: 'ready', value: await client.build(previous.build) });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
setEarlier({ state: 'failed', why: error instanceof Error ? error.message : String(error) });
|
|
40
|
+
}
|
|
41
|
+
}, [client, id]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
void load();
|
|
44
|
+
}, [load]);
|
|
45
|
+
const value = earlier.state === 'ready' ? earlier.value : undefined;
|
|
46
|
+
return useMemo(() => {
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
return earlier;
|
|
49
|
+
const divergence = divergeFrom(build, value);
|
|
50
|
+
const rows = new Map(divergence.shifts.map((each) => [each.subject, each]));
|
|
51
|
+
return {
|
|
52
|
+
state: 'ready',
|
|
53
|
+
earlier: value,
|
|
54
|
+
divergence,
|
|
55
|
+
of: (subject) => rows.get(subject),
|
|
56
|
+
};
|
|
57
|
+
}, [build, value, earlier]);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The run before this one, as the store lists them.
|
|
61
|
+
*
|
|
62
|
+
* By position rather than by clock. Two runs pushed from one machine can carry
|
|
63
|
+
* the same timestamp to the millisecond — both example builds here do — and
|
|
64
|
+
* picking on `at` would compare a build against itself or against its own
|
|
65
|
+
* successor. The listing is newest first and breaks a tied clock by arrival, so
|
|
66
|
+
* position is a total order where the timestamp is not. The build it lands on is
|
|
67
|
+
* named on the page, because a reader who disagrees with the pick can only say so
|
|
68
|
+
* if they can see it.
|
|
69
|
+
*/
|
|
70
|
+
export function previousOf(builds, build) {
|
|
71
|
+
const at = builds.findIndex((each) => each.build === build);
|
|
72
|
+
return at === -1 ? undefined : builds[at + 1];
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=crossing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crossing.js","sourceRoot":"","sources":["../../src/ui/crossing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGlE,OAAO,EAAE,WAAW,EAAiC,MAAM,YAAY,CAAC;AAexE;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,MAAoB,EAAE,KAAkB;IAClE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAKpC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAExB,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC;IAEvB,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,IAAmB,EAAE;QACjD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;YACvD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,UAAU,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;gBAC9B,OAAO;YACT,CAAC;YACD,UAAU,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC5E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,UAAU,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC/F,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;IAEjB,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,IAAI,EAAE,CAAC;IACd,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAEpE,OAAO,OAAO,CAAC,GAAa,EAAE;QAC5B,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,OAAmB,CAAC;QAEpD,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAC5E,OAAO;YACL,KAAK,EAAE,OAAO;YACd,OAAO,EAAE,KAAK;YACd,UAAU;YACV,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;SACnC,CAAC;IACJ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CACxB,MAA+B,EAC/B,KAAa;IAEb,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;IAC5D,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;AAChD,CAAC","sourcesContent":["/**\n * The previous run, fetched once and read by whoever needs a row of it.\n *\n * The crossing used to belong to one panel, which fetched the earlier build for\n * itself and rendered every row of it in one place at the bottom of the build\n * page. Both halves of that were wrong. A reviewer deciding whether to approve a\n * change in `Button` wants one sentence — *build 5 showed you this and nobody\n * decided it* — beside the change, and they will not scroll nine thousand pixels\n * to a section that also tells them about nineteen subjects they are not looking\n * at. And the section itself is worth keeping whole, for the reader who came to\n * ask what moved since Friday.\n *\n * So the fetch is a hook and the fold is [`shift.ts`](./shift.ts): one request,\n * one crossing, and three readings that cannot disagree — the whole table on the\n * run page, the rows under one change on its card, and the single row on a\n * subject.\n */\n\nimport { useCallback, useEffect, useMemo, useState } from 'react';\nimport type { BuildDetail, BuildSummary } from '../review-types.js';\nimport type { ReviewClient } from './client.js';\nimport { divergeFrom, type Divergence, type Shifted } from './shift.js';\n\n/** The earlier run, or the reason there is nothing to place this one against. */\nexport type Crossing =\n | { readonly state: 'loading' }\n | { readonly state: 'none' }\n | { readonly state: 'failed'; readonly why: string }\n | {\n readonly state: 'ready';\n readonly earlier: BuildDetail;\n readonly divergence: Divergence;\n /** This subject's row, or `undefined` when neither run named it. */\n readonly of: (subject: string) => Shifted | undefined;\n };\n\n/**\n * This build against the one before it.\n *\n * A failure is a state and never a silence. An empty crossing reads as *nothing\n * has changed since the last run*, which is the sentence somebody merges on.\n */\nexport function useCrossing(client: ReviewClient, build: BuildDetail): Crossing {\n const [earlier, setEarlier] = useState<\n | { readonly state: 'loading' }\n | { readonly state: 'none' }\n | { readonly state: 'failed'; readonly why: string }\n | { readonly state: 'ready'; readonly value: BuildDetail }\n >({ state: 'loading' });\n\n const id = build.build;\n\n const load = useCallback(async (): Promise<void> => {\n try {\n const previous = previousOf(await client.builds(), id);\n if (previous === undefined) {\n setEarlier({ state: 'none' });\n return;\n }\n setEarlier({ state: 'ready', value: await client.build(previous.build) });\n } catch (error) {\n setEarlier({ state: 'failed', why: error instanceof Error ? error.message : String(error) });\n }\n }, [client, id]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const value = earlier.state === 'ready' ? earlier.value : undefined;\n\n return useMemo((): Crossing => {\n if (value === undefined) return earlier as Crossing;\n\n const divergence = divergeFrom(build, value);\n const rows = new Map(divergence.shifts.map((each) => [each.subject, each]));\n return {\n state: 'ready',\n earlier: value,\n divergence,\n of: (subject) => rows.get(subject),\n };\n }, [build, value, earlier]);\n}\n\n/**\n * The run before this one, as the store lists them.\n *\n * By position rather than by clock. Two runs pushed from one machine can carry\n * the same timestamp to the millisecond — both example builds here do — and\n * picking on `at` would compare a build against itself or against its own\n * successor. The listing is newest first and breaks a tied clock by arrival, so\n * position is a total order where the timestamp is not. The build it lands on is\n * named on the page, because a reader who disagrees with the pick can only say so\n * if they can see it.\n */\nexport function previousOf(\n builds: readonly BuildSummary[],\n build: string,\n): BuildSummary | undefined {\n const at = builds.findIndex((each) => each.build === build);\n return at === -1 ? undefined : builds[at + 1];\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the config declared, and what each declaration did — on the review page.
|
|
3
|
+
*
|
|
4
|
+
* The audit surface, and the one part of a run that a reviewer cannot recover by
|
|
5
|
+
* looking harder at the pictures. An ignore says *part of this page is not my
|
|
6
|
+
* subject*; a sensitivity says *assert on this much of it*. Both make a run less
|
|
7
|
+
* observant on purpose, and both are only safe because something counts what they
|
|
8
|
+
* absorbed: a mask that outlived its cause grows quietly over a real regression,
|
|
9
|
+
* and the only thing that ever catches it is a line saying the rule took nothing
|
|
10
|
+
* again. A run prints that line once, into a terminal nobody keeps. This is where
|
|
11
|
+
* it survives long enough to be *again*.
|
|
12
|
+
*
|
|
13
|
+
* ## The same words the report uses
|
|
14
|
+
*
|
|
15
|
+
* Every state and every sentence here comes from `@variance-authority/report` —
|
|
16
|
+
* {@link ignoreState}, {@link ignoreSays}, {@link ignoreTotals} and their
|
|
17
|
+
* sensitivity twins. Nothing on this page decides what *dead* means. That is the
|
|
18
|
+
* house rule about renderers, and it is load-bearing rather than tidy: an
|
|
19
|
+
* operator reading `dead` in their terminal, `dead` in the HTML report and
|
|
20
|
+
* something else here would have three tools and no answer, and the first
|
|
21
|
+
* version of the HTML table earned exactly that by re-deriving the state from
|
|
22
|
+
* `pixels === 0` — which calls every rule in a fresh checkout dead.
|
|
23
|
+
*
|
|
24
|
+
* ## Absence, which is a third thing
|
|
25
|
+
*
|
|
26
|
+
* `null` is *this build carried no ledger*, and the panel says so in words rather
|
|
27
|
+
* than by rendering nothing. Silence in an audit reads as *audited, and clean*,
|
|
28
|
+
* which is the one sentence a surface like this must never accidentally make.
|
|
29
|
+
*/
|
|
30
|
+
import type { ReactElement } from 'react';
|
|
31
|
+
import type { Declarations } from '../review-types.js';
|
|
32
|
+
/** The card, or the sentence that stands in for it when nothing was carried. */
|
|
33
|
+
export declare function DeclarationsPanel({ declarations, }: {
|
|
34
|
+
readonly declarations: Declarations;
|
|
35
|
+
}): ReactElement;
|
|
36
|
+
//# sourceMappingURL=declarations.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { absorbedBands, ignoreSays, ignoreState, ignoreTotals, isActionable, sensitivitySays, sensitivityState, sensitivityTotals, } from '@variance-authority/report';
|
|
3
|
+
import { number } from './text.js';
|
|
4
|
+
/** The card, or the sentence that stands in for it when nothing was carried. */
|
|
5
|
+
export function DeclarationsPanel({ declarations, }) {
|
|
6
|
+
const { ignores, sensitivities } = declarations;
|
|
7
|
+
const hasIgnores = ignores !== null && ignores.rules.length > 0;
|
|
8
|
+
const hasSensitivities = sensitivities !== null && sensitivities.rules.length > 0;
|
|
9
|
+
if (!hasIgnores && !hasSensitivities) {
|
|
10
|
+
return (_jsx("p", { className: "va-note", children: "This build carried no declaration ledger. Either its config named no ignores and no sensitivities, or it was recorded before this service kept them \u2014 one absence on the wire, and neither of them is an audit that came back clean." }));
|
|
11
|
+
}
|
|
12
|
+
return (_jsxs(_Fragment, { children: [hasIgnores ? _jsx(Ignores, { ledger: ignores }) : null, hasSensitivities ? _jsx(Sensitivities, { ledger: sensitivities }) : null] }));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Every ignore rule, including — especially — the ones that absorbed nothing.
|
|
16
|
+
*
|
|
17
|
+
* Filtering the quiet rows out would delete the finding. Both counts a rule has
|
|
18
|
+
* are printed because one of them is a denominator: *absorbed nothing in nine
|
|
19
|
+
* compared subjects* is a rule to delete, and *absorbed nothing in nine subjects,
|
|
20
|
+
* none of which was compared* is a rule this run knows nothing about.
|
|
21
|
+
*/
|
|
22
|
+
function Ignores({ ledger }) {
|
|
23
|
+
return (_jsxs(_Fragment, { children: [_jsxs("h3", { className: "va-ledger-head", children: ["Ignores ", _jsx("span", { className: "va-note", children: "what each rule absorbed, and what it did not" })] }), _jsx("table", { className: "va-region-table va-ledger", children: _jsx("tbody", { children: ledger.rules.map((entry) => {
|
|
24
|
+
const state = ignoreState(entry);
|
|
25
|
+
return (_jsxs("tr", { className: isActionable(state) ? 'va-spent' : '', children: [_jsx("td", { children: _jsx("code", { children: entry.rule }) }), _jsx("td", { children: _jsx(State, { state: state, says: ignoreSays(entry, ledger) }) }), _jsx("td", { className: "va-num-col", children: entry.pixels === 0 ? (_jsx("span", { className: "va-none", children: "none" })) : (`${number(entry.pixels)} px`) }), _jsx("td", { className: "va-num-col", children: _jsx(Scope, { entry: entry }) }), _jsx("td", { className: "va-ledger-why", children: entry.reason })] }, entry.rule));
|
|
26
|
+
}) }) }), _jsx("p", { className: "va-note", children: ignoreTotals(ledger) })] }));
|
|
27
|
+
}
|
|
28
|
+
/** The two counts, written so neither can be read as the other. */
|
|
29
|
+
function Scope({ entry }) {
|
|
30
|
+
if (entry.subjects === 0)
|
|
31
|
+
return _jsx("span", { className: "va-none", children: "nowhere" });
|
|
32
|
+
return (_jsxs(_Fragment, { children: [number(entry.subjects), " ", _jsx("em", { children: "subj" }), entry.comparedIn === entry.subjects ? null : (_jsxs("span", { className: "va-none", children: [" ", number(entry.comparedIn), " compared"] }))] }));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Every sensitivity, stated in the positive form the operator wrote.
|
|
36
|
+
*
|
|
37
|
+
* *Asserts on layout* is a claim about intent and *decided 38 subjects* is what
|
|
38
|
+
* happened. A table with only the second is a list of things that were hidden;
|
|
39
|
+
* one with only the first is a list of things somebody believes.
|
|
40
|
+
*/
|
|
41
|
+
function Sensitivities({ ledger }) {
|
|
42
|
+
return (_jsxs(_Fragment, { children: [_jsxs("h3", { className: "va-ledger-head", children: ["Sensitivities ", _jsx("span", { className: "va-note", children: "what each rule relaxed, and what it did not" })] }), _jsx("table", { className: "va-region-table va-ledger", children: _jsx("tbody", { children: ledger.rules.map((entry) => {
|
|
43
|
+
const state = sensitivityState(entry);
|
|
44
|
+
return (_jsxs("tr", { className: isActionable(state) ? 'va-spent' : '', children: [_jsx("td", { children: _jsx("code", { children: entry.rule }) }), _jsx("td", { children: _jsx(State, { state: state, says: sensitivitySays(entry) }) }), _jsx("td", { children: _jsxs("span", { className: "va-tag", children: ["asserts on ", entry.level] }) }), _jsx("td", { className: "va-num-col", children: entry.scoped === 0 ? (_jsx("span", { className: "va-none", children: "nowhere" })) : (_jsxs(_Fragment, { children: [number(entry.absorbed.length), " ", _jsx("em", { children: "of" }), " ", number(entry.scoped)] })) }), _jsxs("td", { className: "va-ledger-why", children: [_jsx(Bands, { entry: entry }), entry.reason] })] }, entry.rule));
|
|
45
|
+
}) }) }), _jsx("p", { className: "va-note", children: sensitivityTotals(ledger) })] }));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The bands, or the fact that this record does not have them.
|
|
49
|
+
*
|
|
50
|
+
* A rule that decided verdicts with no band kept is not a rule that absorbed
|
|
51
|
+
* nothing — it is a report written before the bands were kept, or a store that
|
|
52
|
+
* dropped them on the way here. Drawing nothing would say the first.
|
|
53
|
+
*/
|
|
54
|
+
function Bands({ entry }) {
|
|
55
|
+
const found = absorbedBands(entry);
|
|
56
|
+
if (found === undefined) {
|
|
57
|
+
return (_jsx("span", { className: "va-tag", title: "This report did not record which bands were absorbed.", children: "bands not recorded" }));
|
|
58
|
+
}
|
|
59
|
+
if (found.length === 0)
|
|
60
|
+
return null;
|
|
61
|
+
return (_jsx(_Fragment, { children: found.map((band) => (_jsx("span", { className: "va-tag", children: band }, band))) }));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A rule's state as one word, marked when it is a word somebody has to act on.
|
|
65
|
+
*
|
|
66
|
+
* `untested` deliberately reads quiet. It is the absence of evidence, and a panel
|
|
67
|
+
* that flagged it would ask an operator to act on a run that measured nothing —
|
|
68
|
+
* which is how an audit stops being read.
|
|
69
|
+
*/
|
|
70
|
+
function State({ state, says, }) {
|
|
71
|
+
return (_jsx("span", { className: isActionable(state) ? 'va-tag va-warn' : 'va-tag', title: says, children: state }));
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=declarations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"declarations.js","sourceRoot":"","sources":["../../src/ui/declarations.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the config declared, and what each declaration did — on the review page.\n *\n * The audit surface, and the one part of a run that a reviewer cannot recover by\n * looking harder at the pictures. An ignore says *part of this page is not my\n * subject*; a sensitivity says *assert on this much of it*. Both make a run less\n * observant on purpose, and both are only safe because something counts what they\n * absorbed: a mask that outlived its cause grows quietly over a real regression,\n * and the only thing that ever catches it is a line saying the rule took nothing\n * again. A run prints that line once, into a terminal nobody keeps. This is where\n * it survives long enough to be *again*.\n *\n * ## The same words the report uses\n *\n * Every state and every sentence here comes from `@variance-authority/report` —\n * {@link ignoreState}, {@link ignoreSays}, {@link ignoreTotals} and their\n * sensitivity twins. Nothing on this page decides what *dead* means. That is the\n * house rule about renderers, and it is load-bearing rather than tidy: an\n * operator reading `dead` in their terminal, `dead` in the HTML report and\n * something else here would have three tools and no answer, and the first\n * version of the HTML table earned exactly that by re-deriving the state from\n * `pixels === 0` — which calls every rule in a fresh checkout dead.\n *\n * ## Absence, which is a third thing\n *\n * `null` is *this build carried no ledger*, and the panel says so in words rather\n * than by rendering nothing. Silence in an audit reads as *audited, and clean*,\n * which is the one sentence a surface like this must never accidentally make.\n */\n\nimport type { ReactElement } from 'react';\nimport {\n absorbedBands,\n ignoreSays,\n ignoreState,\n ignoreTotals,\n isActionable,\n sensitivitySays,\n sensitivityState,\n sensitivityTotals,\n type IgnoreLedger,\n type IgnoreUsage,\n type SensitivityLedger,\n type SensitivityUsage,\n} from '@variance-authority/report';\nimport type { Declarations } from '../review-types.js';\nimport { number } from './text.js';\n\n/** The card, or the sentence that stands in for it when nothing was carried. */\nexport function DeclarationsPanel({\n declarations,\n}: {\n readonly declarations: Declarations;\n}): ReactElement {\n const { ignores, sensitivities } = declarations;\n const hasIgnores = ignores !== null && ignores.rules.length > 0;\n const hasSensitivities = sensitivities !== null && sensitivities.rules.length > 0;\n\n if (!hasIgnores && !hasSensitivities) {\n return (\n <p className=\"va-note\">\n This build carried no declaration ledger. Either its config named no ignores and no\n sensitivities, or it was recorded before this service kept them — one absence on the wire,\n and neither of them is an audit that came back clean.\n </p>\n );\n }\n\n return (\n <>\n {hasIgnores ? <Ignores ledger={ignores} /> : null}\n {hasSensitivities ? <Sensitivities ledger={sensitivities} /> : null}\n </>\n );\n}\n\n/**\n * Every ignore rule, including — especially — the ones that absorbed nothing.\n *\n * Filtering the quiet rows out would delete the finding. Both counts a rule has\n * are printed because one of them is a denominator: *absorbed nothing in nine\n * compared subjects* is a rule to delete, and *absorbed nothing in nine subjects,\n * none of which was compared* is a rule this run knows nothing about.\n */\nfunction Ignores({ ledger }: { readonly ledger: IgnoreLedger }): ReactElement {\n return (\n <>\n <h3 className=\"va-ledger-head\">\n Ignores <span className=\"va-note\">what each rule absorbed, and what it did not</span>\n </h3>\n <table className=\"va-region-table va-ledger\">\n <tbody>\n {ledger.rules.map((entry) => {\n const state = ignoreState(entry);\n return (\n <tr key={entry.rule} className={isActionable(state) ? 'va-spent' : ''}>\n <td>\n <code>{entry.rule}</code>\n </td>\n <td>\n <State state={state} says={ignoreSays(entry, ledger)} />\n </td>\n <td className=\"va-num-col\">\n {entry.pixels === 0 ? (\n <span className=\"va-none\">none</span>\n ) : (\n `${number(entry.pixels)} px`\n )}\n </td>\n <td className=\"va-num-col\">\n <Scope entry={entry} />\n </td>\n <td className=\"va-ledger-why\">{entry.reason}</td>\n </tr>\n );\n })}\n </tbody>\n </table>\n <p className=\"va-note\">{ignoreTotals(ledger)}</p>\n </>\n );\n}\n\n/** The two counts, written so neither can be read as the other. */\nfunction Scope({ entry }: { readonly entry: IgnoreUsage }): ReactElement {\n if (entry.subjects === 0) return <span className=\"va-none\">nowhere</span>;\n return (\n <>\n {number(entry.subjects)} <em>subj</em>\n {entry.comparedIn === entry.subjects ? null : (\n <span className=\"va-none\"> {number(entry.comparedIn)} compared</span>\n )}\n </>\n );\n}\n\n/**\n * Every sensitivity, stated in the positive form the operator wrote.\n *\n * *Asserts on layout* is a claim about intent and *decided 38 subjects* is what\n * happened. A table with only the second is a list of things that were hidden;\n * one with only the first is a list of things somebody believes.\n */\nfunction Sensitivities({ ledger }: { readonly ledger: SensitivityLedger }): ReactElement {\n return (\n <>\n <h3 className=\"va-ledger-head\">\n Sensitivities <span className=\"va-note\">what each rule relaxed, and what it did not</span>\n </h3>\n <table className=\"va-region-table va-ledger\">\n <tbody>\n {ledger.rules.map((entry) => {\n const state = sensitivityState(entry);\n return (\n <tr key={entry.rule} className={isActionable(state) ? 'va-spent' : ''}>\n <td>\n <code>{entry.rule}</code>\n </td>\n <td>\n <State state={state} says={sensitivitySays(entry)} />\n </td>\n <td>\n <span className=\"va-tag\">asserts on {entry.level}</span>\n </td>\n <td className=\"va-num-col\">\n {entry.scoped === 0 ? (\n <span className=\"va-none\">nowhere</span>\n ) : (\n <>\n {number(entry.absorbed.length)} <em>of</em> {number(entry.scoped)}\n </>\n )}\n </td>\n <td className=\"va-ledger-why\">\n <Bands entry={entry} />\n {entry.reason}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n <p className=\"va-note\">{sensitivityTotals(ledger)}</p>\n </>\n );\n}\n\n/**\n * The bands, or the fact that this record does not have them.\n *\n * A rule that decided verdicts with no band kept is not a rule that absorbed\n * nothing — it is a report written before the bands were kept, or a store that\n * dropped them on the way here. Drawing nothing would say the first.\n */\nfunction Bands({ entry }: { readonly entry: SensitivityUsage }): ReactElement | null {\n const found = absorbedBands(entry);\n if (found === undefined) {\n return (\n <span className=\"va-tag\" title=\"This report did not record which bands were absorbed.\">\n bands not recorded\n </span>\n );\n }\n if (found.length === 0) return null;\n return (\n <>\n {found.map((band) => (\n <span key={band} className=\"va-tag\">\n {band}\n </span>\n ))}\n </>\n );\n}\n\n/**\n * A rule's state as one word, marked when it is a word somebody has to act on.\n *\n * `untested` deliberately reads quiet. It is the absence of evidence, and a panel\n * that flagged it would ask an operator to act on a run that measured nothing —\n * which is how an audit stops being read.\n */\nfunction State({\n state,\n says,\n}: {\n readonly state: Parameters<typeof isActionable>[0];\n readonly says: string;\n}): ReactElement {\n return (\n <span className={isActionable(state) ? 'va-tag va-warn' : 'va-tag'} title={says}>\n {state}\n </span>\n );\n}\n"]}
|