@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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the run decided about each thing that moved, addressed by the pair.
|
|
3
|
+
*
|
|
4
|
+
* The build page used to answer *why did this move* by walking the census
|
|
5
|
+
* itself — up from the component that moved, through what encloses it, to a name
|
|
6
|
+
* the diff reaches. That walk was a reconstruction, and it worked from the
|
|
7
|
+
* thinner half of the record: the census carries edges, and the run that wrote it
|
|
8
|
+
* also had the diff, the source index that maps a component to the file declaring
|
|
9
|
+
* it, the props digest each rendering was grouped under, and the subjects where
|
|
10
|
+
* the same component with the same props held still. It reached a conclusion from
|
|
11
|
+
* all four and wrote one sentence per movement. None of it crossed the wire.
|
|
12
|
+
*
|
|
13
|
+
* So the page asks rather than re-derives. What is here is the addressing — a
|
|
14
|
+
* movement is about a *pair*, and the two questions a surface asks of it are
|
|
15
|
+
* *what happened to this component here* and *what happened to it anywhere* —
|
|
16
|
+
* plus the one fold that turns a component's several answers into the band it is
|
|
17
|
+
* read under.
|
|
18
|
+
*
|
|
19
|
+
* ## The pair is not a formality
|
|
20
|
+
*
|
|
21
|
+
* The same `Button` is `edited` on the page whose file the diff names and
|
|
22
|
+
* `upstream` on the page where a changed parent hands it a different label. A
|
|
23
|
+
* surface keyed by component alone shows one of those and is wrong about the
|
|
24
|
+
* other, and which one it is wrong about depends on iteration order.
|
|
25
|
+
*/
|
|
26
|
+
const NONE = [];
|
|
27
|
+
export function attributionsOf(build) {
|
|
28
|
+
const pairs = new Map();
|
|
29
|
+
const byComponent = new Map();
|
|
30
|
+
for (const movement of build.movements) {
|
|
31
|
+
pairs.set(key(movement.component, movement.subject), movement);
|
|
32
|
+
const found = byComponent.get(movement.component);
|
|
33
|
+
if (found === undefined)
|
|
34
|
+
byComponent.set(movement.component, [movement]);
|
|
35
|
+
else
|
|
36
|
+
found.push(movement);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
at: (component, subject) => pairs.get(key(component, subject)),
|
|
40
|
+
about: (component) => byComponent.get(component) ?? NONE,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// A separator neither half can contain, so a `Card` moving in `a<NUL>b` and a
|
|
44
|
+
// `Card<NUL>a` moving in `b` cannot arrive at one key. Written as the escape and
|
|
45
|
+
// not as the byte: a source file with a NUL in it is binary to `grep`, and a
|
|
46
|
+
// search that finds nothing in a file that contains the word is the exact
|
|
47
|
+
// failure this repository exists to refuse.
|
|
48
|
+
const IN = '\u0000';
|
|
49
|
+
function key(component, subject) {
|
|
50
|
+
return `${component}${IN}${subject}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Ladder order — the order the run climbs, so the strongest explanation first.
|
|
54
|
+
*
|
|
55
|
+
* Used to fold a component's several answers into one, and the direction is the
|
|
56
|
+
* point. A component that is `edited` in one render and `upstream` in another was
|
|
57
|
+
* edited: the second render is the same edit arriving somewhere else, and a fold
|
|
58
|
+
* that took the weaker rung would file the whole change under a consequence of
|
|
59
|
+
* itself.
|
|
60
|
+
*/
|
|
61
|
+
const LADDER = ['edited', 'token', 'upstream', 'contradicted', 'unexplained'];
|
|
62
|
+
/**
|
|
63
|
+
* One component's rung, over every render it moved in.
|
|
64
|
+
*
|
|
65
|
+
* `undefined` when the run recorded nothing about it — which is not a rung and
|
|
66
|
+
* must never be drawn as one. A build ingested before attributions were carried
|
|
67
|
+
* returns this for everything, and a reader that turned it into *unexplained*
|
|
68
|
+
* would be reporting a missing input as a finding.
|
|
69
|
+
*/
|
|
70
|
+
export function rungAcross(movements) {
|
|
71
|
+
let best;
|
|
72
|
+
for (const movement of movements) {
|
|
73
|
+
if (best === undefined || LADDER.indexOf(movement.cause) < LADDER.indexOf(best)) {
|
|
74
|
+
best = movement.cause;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return best;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=attribution.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attribution.js","sourceRoot":"","sources":["../../src/ui/attribution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAeH,MAAM,IAAI,GAA4B,EAAE,CAAC;AAEzC,MAAM,UAAU,cAAc,CAAC,KAAkB;IAC/C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC9C,MAAM,WAAW,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEtD,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QACvC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QAClD,IAAI,KAAK,KAAK,SAAS;YAAE,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;;YACpE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC;IAED,OAAO;QACL,EAAE,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAC9D,KAAK,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI;KACzD,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,iFAAiF;AACjF,6EAA6E;AAC7E,0EAA0E;AAC1E,4CAA4C;AAC5C,MAAM,EAAE,GAAG,QAAQ,CAAC;AAEpB,SAAS,GAAG,CAAC,SAAiB,EAAE,OAAe;IAC7C,OAAO,GAAG,SAAS,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC;AACvC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,GAAoB,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,CAAC,CAAC;AAE/F;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,SAAkC;IAC3D,IAAI,IAAsB,CAAC;IAC3B,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,IAAI,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAChF,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC;QACxB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * What the run decided about each thing that moved, addressed by the pair.\n *\n * The build page used to answer *why did this move* by walking the census\n * itself — up from the component that moved, through what encloses it, to a name\n * the diff reaches. That walk was a reconstruction, and it worked from the\n * thinner half of the record: the census carries edges, and the run that wrote it\n * also had the diff, the source index that maps a component to the file declaring\n * it, the props digest each rendering was grouped under, and the subjects where\n * the same component with the same props held still. It reached a conclusion from\n * all four and wrote one sentence per movement. None of it crossed the wire.\n *\n * So the page asks rather than re-derives. What is here is the addressing — a\n * movement is about a *pair*, and the two questions a surface asks of it are\n * *what happened to this component here* and *what happened to it anywhere* —\n * plus the one fold that turns a component's several answers into the band it is\n * read under.\n *\n * ## The pair is not a formality\n *\n * The same `Button` is `edited` on the page whose file the diff names and\n * `upstream` on the page where a changed parent hands it a different label. A\n * surface keyed by component alone shows one of those and is wrong about the\n * other, and which one it is wrong about depends on iteration order.\n */\n\nimport type { BuildDetail, MovementView } from '../review-types.js';\n\n/** The five rungs, as the report's ladder names them. */\nexport type Rung = MovementView['cause'];\n\n/** One build's attributions, indexed the two ways a surface asks for them. */\nexport interface Attributions {\n /** What the run concluded about this component in this render. */\n readonly at: (component: string, subject: string) => MovementView | undefined;\n /** Every conclusion about this component, in the order the store returned. */\n readonly about: (component: string) => readonly MovementView[];\n}\n\nconst NONE: readonly MovementView[] = [];\n\nexport function attributionsOf(build: BuildDetail): Attributions {\n const pairs = new Map<string, MovementView>();\n const byComponent = new Map<string, MovementView[]>();\n\n for (const movement of build.movements) {\n pairs.set(key(movement.component, movement.subject), movement);\n const found = byComponent.get(movement.component);\n if (found === undefined) byComponent.set(movement.component, [movement]);\n else found.push(movement);\n }\n\n return {\n at: (component, subject) => pairs.get(key(component, subject)),\n about: (component) => byComponent.get(component) ?? NONE,\n };\n}\n\n// A separator neither half can contain, so a `Card` moving in `a<NUL>b` and a\n// `Card<NUL>a` moving in `b` cannot arrive at one key. Written as the escape and\n// not as the byte: a source file with a NUL in it is binary to `grep`, and a\n// search that finds nothing in a file that contains the word is the exact\n// failure this repository exists to refuse.\nconst IN = '\\u0000';\n\nfunction key(component: string, subject: string): string {\n return `${component}${IN}${subject}`;\n}\n\n/**\n * Ladder order — the order the run climbs, so the strongest explanation first.\n *\n * Used to fold a component's several answers into one, and the direction is the\n * point. A component that is `edited` in one render and `upstream` in another was\n * edited: the second render is the same edit arriving somewhere else, and a fold\n * that took the weaker rung would file the whole change under a consequence of\n * itself.\n */\nconst LADDER: readonly Rung[] = ['edited', 'token', 'upstream', 'contradicted', 'unexplained'];\n\n/**\n * One component's rung, over every render it moved in.\n *\n * `undefined` when the run recorded nothing about it — which is not a rung and\n * must never be drawn as one. A build ingested before attributions were carried\n * returns this for everything, and a reader that turned it into *unexplained*\n * would be reporting a missing input as a finding.\n */\nexport function rungAcross(movements: readonly MovementView[]): Rung | undefined {\n let best: Rung | undefined;\n for (const movement of movements) {\n if (best === undefined || LADDER.indexOf(movement.cause) < LADDER.indexOf(best)) {\n best = movement.cause;\n }\n }\n return best;\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run's own answer to *why did this move*, printed where the decision is made.
|
|
3
|
+
*
|
|
4
|
+
* Every other sentence on the change page is the page reasoning about records.
|
|
5
|
+
* This one is not: the run wrote it, from four things this surface never
|
|
6
|
+
* receives — the diff, the source index that maps a component to the file
|
|
7
|
+
* declaring it, the props digest each rendering was grouped under, and the
|
|
8
|
+
* subjects where the same component with the same props held still. It reached a
|
|
9
|
+
* conclusion and put it in one sentence per movement, and until the store carried
|
|
10
|
+
* those, the page answered the question by climbing the census on its own and
|
|
11
|
+
* arriving somewhere weaker.
|
|
12
|
+
*
|
|
13
|
+
* ## The sentence is the report's, not a second one
|
|
14
|
+
*
|
|
15
|
+
* `because` is written once, in the attribution ladder, and shipped to the
|
|
16
|
+
* terminal and to here. What differs is the rendering: the ladder marks the names
|
|
17
|
+
* it is claiming things about with backticks, and this turns those into `code`
|
|
18
|
+
* rather than reprinting the punctuation. A second sentence written for the web
|
|
19
|
+
* is a second thing that can disagree with the first, and the disagreement would
|
|
20
|
+
* be between a reviewer's screen and their CI log.
|
|
21
|
+
*
|
|
22
|
+
* ## What the link is for
|
|
23
|
+
*
|
|
24
|
+
* On the `upstream` rung the sentence names a component the commit edited, and
|
|
25
|
+
* that component is usually a change on this same docket. *This moved on what
|
|
26
|
+
* `ProductCard` gave it* is only half an instruction; the other half is that
|
|
27
|
+
* `ProductCard` is where the decision belongs, and it is one click away.
|
|
28
|
+
*/
|
|
29
|
+
import type { ReactElement } from 'react';
|
|
30
|
+
import type { Origin } from './grouping.js';
|
|
31
|
+
import type { Route } from './route.js';
|
|
32
|
+
export declare function Because({ origin, build, changes, go, }: {
|
|
33
|
+
readonly origin: Origin;
|
|
34
|
+
readonly build: string;
|
|
35
|
+
/** Components with a change page of their own, so a link goes somewhere. */
|
|
36
|
+
readonly changes: ReadonlySet<string>;
|
|
37
|
+
readonly go: (route: Route) => void;
|
|
38
|
+
}): ReactElement | null;
|
|
39
|
+
/**
|
|
40
|
+
* One sentence, with the names it claims things about drawn as code.
|
|
41
|
+
*
|
|
42
|
+
* The ladder writes them between backticks because it is also written for a
|
|
43
|
+
* terminal. Splitting on the pair rather than parsing anything: the sentences are
|
|
44
|
+
* this repository's own, an unbalanced backtick in one is a defect in the
|
|
45
|
+
* sentence, and the failure mode here — a stray backtick surviving into the
|
|
46
|
+
* output — is visible in the place it would be introduced.
|
|
47
|
+
*/
|
|
48
|
+
export declare function Marked({ say }: {
|
|
49
|
+
readonly say: string;
|
|
50
|
+
}): ReactElement;
|
|
51
|
+
//# sourceMappingURL=because.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Go } from './shell.js';
|
|
3
|
+
import { count, number } from './text.js';
|
|
4
|
+
export function Because({ origin, build, changes, go, }) {
|
|
5
|
+
const recorded = origin.appearances.flatMap(({ movement }) => movement ?? []);
|
|
6
|
+
if (recorded.length === 0)
|
|
7
|
+
return null;
|
|
8
|
+
return (_jsx(_Fragment, { children: reasons(recorded).map(({ movement, renders }) => (_jsxs("p", { className: `va-attributed va-attributed-${movement.cause}`, children: [_jsx(Sentence, { movement: movement, build: build, changes: changes, go: go }), renders === origin.appearances.length ? null : (_jsxs("span", { className: "va-note", children: [' ', "\u2014 in ", count(renders, 'render'), " of ", number(origin.appearances.length), "."] }))] }, movement.because))) }));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The distinct answers, widest first.
|
|
12
|
+
*
|
|
13
|
+
* Usually one. When it is not, the reason is worth the second paragraph: the same
|
|
14
|
+
* `Button` is `edited` on the page whose file the diff names and `upstream` on the
|
|
15
|
+
* page where a changed parent hands it a different label, and a page that printed
|
|
16
|
+
* only the first would be telling a reviewer they edited a render they did not.
|
|
17
|
+
*/
|
|
18
|
+
function reasons(movements) {
|
|
19
|
+
const found = new Map();
|
|
20
|
+
for (const movement of movements) {
|
|
21
|
+
const seen = found.get(movement.because);
|
|
22
|
+
if (seen === undefined)
|
|
23
|
+
found.set(movement.because, { movement, renders: 1 });
|
|
24
|
+
else
|
|
25
|
+
seen.renders += 1;
|
|
26
|
+
}
|
|
27
|
+
return [...found.values()].sort((left, right) => right.renders - left.renders);
|
|
28
|
+
}
|
|
29
|
+
function Sentence({ movement, build, changes, go, }) {
|
|
30
|
+
const upstream = movement.upstream;
|
|
31
|
+
const linked = upstream !== undefined && changes.has(upstream);
|
|
32
|
+
return (_jsxs(_Fragment, { children: [_jsx(Marked, { say: movement.because }), movement.standing === undefined ? null : (_jsx("span", { className: movement.standing === 'flake' ? ' va-mark va-alarm' : ' va-mark va-note', children: movement.standing === 'flake'
|
|
33
|
+
? 'this render already failed to read the same way twice'
|
|
34
|
+
: 'nobody has read this render twice yet' })), linked ? (_jsxs(_Fragment, { children: [' ', _jsxs(Go, { to: { page: 'change', build, change: upstream }, go: go, className: "va-attributed-go", title: `The decision belongs to ${upstream}, which is a change on this build`, children: ["Decide it under ", upstream] })] })) : null] }));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* One sentence, with the names it claims things about drawn as code.
|
|
38
|
+
*
|
|
39
|
+
* The ladder writes them between backticks because it is also written for a
|
|
40
|
+
* terminal. Splitting on the pair rather than parsing anything: the sentences are
|
|
41
|
+
* this repository's own, an unbalanced backtick in one is a defect in the
|
|
42
|
+
* sentence, and the failure mode here — a stray backtick surviving into the
|
|
43
|
+
* output — is visible in the place it would be introduced.
|
|
44
|
+
*/
|
|
45
|
+
export function Marked({ say }) {
|
|
46
|
+
// Keyed by position, which is the identity: the list is one sentence cut into
|
|
47
|
+
// its own pieces, and it is rebuilt whole or not at all.
|
|
48
|
+
return (_jsx(_Fragment, { children: say
|
|
49
|
+
.split('`')
|
|
50
|
+
.map((part, index) => index % 2 === 0 ? _jsx("span", { children: part }, index) : _jsx("code", { children: part }, index)) }));
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=because.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"because.js","sourceRoot":"","sources":["../../src/ui/because.tsx"],"names":[],"mappings":";AAiCA,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAChC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAE1C,MAAM,UAAU,OAAO,CAAC,EACtB,MAAM,EACN,KAAK,EACL,OAAO,EACP,EAAE,GAOH;IACC,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;IAC9E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvC,OAAO,CACL,4BACG,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAChD,aAA0B,SAAS,EAAE,+BAA+B,QAAQ,CAAC,KAAK,EAAE,aAClF,KAAC,QAAQ,IAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,GAAI,EACvE,OAAO,KAAK,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC9C,gBAAM,SAAS,EAAC,SAAS,aACtB,GAAG,gBACE,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,UAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,SAChE,CACR,KAPK,QAAQ,CAAC,OAAO,CAQpB,CACL,CAAC,GACD,CACJ,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,OAAO,CACd,SAAkC;IAElC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuD,CAAC;IAC7E,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACzC,IAAI,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;;YACzE,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;AACjF,CAAC;AAED,SAAS,QAAQ,CAAC,EAChB,QAAQ,EACR,KAAK,EACL,OAAO,EACP,EAAE,GAMH;IACC,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC;IACnC,MAAM,MAAM,GAAG,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAE/D,OAAO,CACL,8BACE,KAAC,MAAM,IAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,GAAI,EAChC,QAAQ,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACxC,eAAM,SAAS,EAAE,QAAQ,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,kBAAkB,YACtF,QAAQ,CAAC,QAAQ,KAAK,OAAO;oBAC5B,CAAC,CAAC,uDAAuD;oBACzD,CAAC,CAAC,uCAAuC,GACtC,CACR,EACA,MAAM,CAAC,CAAC,CAAC,CACR,8BACG,GAAG,EACJ,MAAC,EAAE,IACD,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,EAC/C,EAAE,EAAE,EAAE,EACN,SAAS,EAAC,kBAAkB,EAC5B,KAAK,EAAE,2BAA2B,QAAQ,mCAAmC,iCAE5D,QAAQ,IACtB,IACJ,CACJ,CAAC,CAAC,CAAC,IAAI,IACP,CACJ,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,MAAM,CAAC,EAAE,GAAG,EAA4B;IACtD,8EAA8E;IAC9E,yDAAyD;IACzD,OAAO,CACL,4BACG,GAAG;aACD,KAAK,CAAC,GAAG,CAAC;aACV,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CACnB,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,yBAAmB,IAAI,IAAZ,KAAK,CAAe,CAAC,CAAC,CAAC,yBAAmB,IAAI,IAAZ,KAAK,CAAe,CACpF,GACF,CACJ,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The run's own answer to *why did this move*, printed where the decision is made.\n *\n * Every other sentence on the change page is the page reasoning about records.\n * This one is not: the run wrote it, from four things this surface never\n * receives — the diff, the source index that maps a component to the file\n * declaring it, the props digest each rendering was grouped under, and the\n * subjects where the same component with the same props held still. It reached a\n * conclusion and put it in one sentence per movement, and until the store carried\n * those, the page answered the question by climbing the census on its own and\n * arriving somewhere weaker.\n *\n * ## The sentence is the report's, not a second one\n *\n * `because` is written once, in the attribution ladder, and shipped to the\n * terminal and to here. What differs is the rendering: the ladder marks the names\n * it is claiming things about with backticks, and this turns those into `code`\n * rather than reprinting the punctuation. A second sentence written for the web\n * is a second thing that can disagree with the first, and the disagreement would\n * be between a reviewer's screen and their CI log.\n *\n * ## What the link is for\n *\n * On the `upstream` rung the sentence names a component the commit edited, and\n * that component is usually a change on this same docket. *This moved on what\n * `ProductCard` gave it* is only half an instruction; the other half is that\n * `ProductCard` is where the decision belongs, and it is one click away.\n */\n\nimport type { ReactElement } from 'react';\nimport type { MovementView } from '../review-types.js';\nimport type { Origin } from './grouping.js';\nimport type { Route } from './route.js';\nimport { Go } from './shell.js';\nimport { count, number } from './text.js';\n\nexport function Because({\n origin,\n build,\n changes,\n go,\n}: {\n readonly origin: Origin;\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 const recorded = origin.appearances.flatMap(({ movement }) => movement ?? []);\n if (recorded.length === 0) return null;\n\n return (\n <>\n {reasons(recorded).map(({ movement, renders }) => (\n <p key={movement.because} className={`va-attributed va-attributed-${movement.cause}`}>\n <Sentence movement={movement} build={build} changes={changes} go={go} />\n {renders === origin.appearances.length ? null : (\n <span className=\"va-note\">\n {' '}\n — in {count(renders, 'render')} of {number(origin.appearances.length)}.\n </span>\n )}\n </p>\n ))}\n </>\n );\n}\n\n/**\n * The distinct answers, widest first.\n *\n * Usually one. When it is not, the reason is worth the second paragraph: the same\n * `Button` is `edited` on the page whose file the diff names and `upstream` on the\n * page where a changed parent hands it a different label, and a page that printed\n * only the first would be telling a reviewer they edited a render they did not.\n */\nfunction reasons(\n movements: readonly MovementView[],\n): readonly { movement: MovementView; renders: number }[] {\n const found = new Map<string, { movement: MovementView; renders: number }>();\n for (const movement of movements) {\n const seen = found.get(movement.because);\n if (seen === undefined) found.set(movement.because, { movement, renders: 1 });\n else seen.renders += 1;\n }\n return [...found.values()].sort((left, right) => right.renders - left.renders);\n}\n\nfunction Sentence({\n movement,\n build,\n changes,\n go,\n}: {\n readonly movement: MovementView;\n readonly build: string;\n readonly changes: ReadonlySet<string>;\n readonly go: (route: Route) => void;\n}): ReactElement {\n const upstream = movement.upstream;\n const linked = upstream !== undefined && changes.has(upstream);\n\n return (\n <>\n <Marked say={movement.because} />\n {movement.standing === undefined ? null : (\n <span className={movement.standing === 'flake' ? ' va-mark va-alarm' : ' va-mark va-note'}>\n {movement.standing === 'flake'\n ? 'this render already failed to read the same way twice'\n : 'nobody has read this render twice yet'}\n </span>\n )}\n {linked ? (\n <>\n {' '}\n <Go\n to={{ page: 'change', build, change: upstream }}\n go={go}\n className=\"va-attributed-go\"\n title={`The decision belongs to ${upstream}, which is a change on this build`}\n >\n Decide it under {upstream}\n </Go>\n </>\n ) : null}\n </>\n );\n}\n\n/**\n * One sentence, with the names it claims things about drawn as code.\n *\n * The ladder writes them between backticks because it is also written for a\n * terminal. Splitting on the pair rather than parsing anything: the sentences are\n * this repository's own, an unbalanced backtick in one is a defect in the\n * sentence, and the failure mode here — a stray backtick surviving into the\n * output — is visible in the place it would be introduced.\n */\nexport function Marked({ say }: { readonly say: string }): ReactElement {\n // Keyed by position, which is the identity: the list is one sentence cut into\n // its own pieces, and it is rebuilt whole or not at all.\n return (\n <>\n {say\n .split('`')\n .map((part, index) =>\n index % 2 === 0 ? <span key={index}>{part}</span> : <code key={index}>{part}</code>,\n )}\n </>\n );\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { StrictMode } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { createReviewClient } from './client.js';
|
|
5
|
+
import { ReviewApp } from './review.js';
|
|
6
|
+
/**
|
|
7
|
+
* The half that runs in a browser, for hosts that do not have a framework.
|
|
8
|
+
*
|
|
9
|
+
* A Next.js app renders {@link ReviewApp} itself and never loads this — it has a
|
|
10
|
+
* bundler, a router and a hydration story of its own, and handing it a
|
|
11
|
+
* pre-bundled React would give the page two of them. This file exists for the
|
|
12
|
+
* Node service, which has none of that: it serves a document and needs one script
|
|
13
|
+
* that turns it into the surface.
|
|
14
|
+
*
|
|
15
|
+
* Bundled at this repository's build time by `tools/tribunal-ui.mjs` into
|
|
16
|
+
* `dist/ui/review.bundle.js`, for the reason recorded in
|
|
17
|
+
* [`node/ui-assets.ts`](../node/ui-assets.ts).
|
|
18
|
+
*
|
|
19
|
+
* It reads its configuration from the document rather than taking it baked in,
|
|
20
|
+
* because the bundle is built once and every deployment mounts the API somewhere
|
|
21
|
+
* else. It reads **no token**: the host attaches the capability, and this script
|
|
22
|
+
* calls its own origin with no credential.
|
|
23
|
+
*/
|
|
24
|
+
const mount = document.getElementById('variance-review');
|
|
25
|
+
const configuration = document.getElementById('variance-config');
|
|
26
|
+
if (mount === null) {
|
|
27
|
+
throw new Error('the review page has no #variance-review element to mount into');
|
|
28
|
+
}
|
|
29
|
+
if (configuration === null) {
|
|
30
|
+
throw new Error('the review page has no #variance-config script to read its endpoint from');
|
|
31
|
+
}
|
|
32
|
+
const { endpoint, reviewer } = JSON.parse(configuration.textContent ?? '{}');
|
|
33
|
+
createRoot(mount).render(_jsx(StrictMode, { children: _jsx(ReviewApp, { client: createReviewClient({ endpoint: endpoint ?? '' }), reviewer: reviewer ?? 'reviewer' }) }));
|
|
34
|
+
//# sourceMappingURL=browser-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-entry.js","sourceRoot":"","sources":["../../src/ui/browser-entry.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,KAAK,GAAG,QAAQ,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;AACzD,MAAM,aAAa,GAAG,QAAQ,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;AAEjE,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;IACnB,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;AACnF,CAAC;AACD,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;IAC3B,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;AAC9F,CAAC;AAED,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,WAAW,IAAI,IAAI,CAG1E,CAAC;AAEF,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CACtB,KAAC,UAAU,cACT,KAAC,SAAS,IACR,MAAM,EAAE,kBAAkB,CAAC,EAAE,QAAQ,EAAE,QAAQ,IAAI,EAAE,EAAE,CAAC,EACxD,QAAQ,EAAE,QAAQ,IAAI,UAAU,GAChC,GACS,CACd,CAAC","sourcesContent":["import { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { createReviewClient } from './client.js';\nimport { ReviewApp } from './review.js';\n\n/**\n * The half that runs in a browser, for hosts that do not have a framework.\n *\n * A Next.js app renders {@link ReviewApp} itself and never loads this — it has a\n * bundler, a router and a hydration story of its own, and handing it a\n * pre-bundled React would give the page two of them. This file exists for the\n * Node service, which has none of that: it serves a document and needs one script\n * that turns it into the surface.\n *\n * Bundled at this repository's build time by `tools/tribunal-ui.mjs` into\n * `dist/ui/review.bundle.js`, for the reason recorded in\n * [`node/ui-assets.ts`](../node/ui-assets.ts).\n *\n * It reads its configuration from the document rather than taking it baked in,\n * because the bundle is built once and every deployment mounts the API somewhere\n * else. It reads **no token**: the host attaches the capability, and this script\n * calls its own origin with no credential.\n */\n\nconst mount = document.getElementById('variance-review');\nconst configuration = document.getElementById('variance-config');\n\nif (mount === null) {\n throw new Error('the review page has no #variance-review element to mount into');\n}\nif (configuration === null) {\n throw new Error('the review page has no #variance-config script to read its endpoint from');\n}\n\nconst { endpoint, reviewer } = JSON.parse(configuration.textContent ?? '{}') as {\n readonly endpoint?: string;\n readonly reviewer?: string;\n};\n\ncreateRoot(mount).render(\n <StrictMode>\n <ReviewApp\n client={createReviewClient({ endpoint: endpoint ?? '' })}\n reviewer={reviewer ?? 'reviewer'}\n />\n </StrictMode>,\n);\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The front page: every build the store has, newest first.
|
|
3
|
+
*
|
|
4
|
+
* A list, and deliberately only a list. What a reader wants here is which build
|
|
5
|
+
* to open, and the two facts that decide it are how many subjects still have
|
|
6
|
+
* nobody's name against them and whether the run observed what it planned to.
|
|
7
|
+
* Everything else about a build is on the build.
|
|
8
|
+
*/
|
|
9
|
+
import { type ReactElement } from 'react';
|
|
10
|
+
import type { BuildSummary } from '../review-types.js';
|
|
11
|
+
import type { ReviewClient } from './client.js';
|
|
12
|
+
import type { Route } from './route.js';
|
|
13
|
+
export declare function BuildsPage({ client, limit, go, }: {
|
|
14
|
+
readonly client: ReviewClient;
|
|
15
|
+
readonly limit?: number | undefined;
|
|
16
|
+
readonly go: (route: Route) => void;
|
|
17
|
+
}): ReactElement;
|
|
18
|
+
export declare function BuildList({ builds, go, }: {
|
|
19
|
+
readonly builds: readonly BuildSummary[];
|
|
20
|
+
readonly go: (route: Route) => void;
|
|
21
|
+
}): ReactElement;
|
|
22
|
+
/**
|
|
23
|
+
* How much of the suite this run looked at, and what happened to the rest.
|
|
24
|
+
*
|
|
25
|
+
* **The denominator is the line.** A run that narrowed twenty subjects to two
|
|
26
|
+
* has done the most expensive thinking in the product, and a page reading
|
|
27
|
+
* `2 subjects` reports a suite of two. `2 of 20 observed` is the same fact with
|
|
28
|
+
* the eighteen still in it.
|
|
29
|
+
*
|
|
30
|
+
* The states are not two. A run that stated its coverage and skipped nothing is
|
|
31
|
+
* clean; a run that stated it and failed on fifty is not; and a run that never
|
|
32
|
+
* said is **unknown**, which must not be drawn as the first. That is the collapse
|
|
33
|
+
* `RunReport.notObserved` exists to prevent, and drawing `0 failed` for a silent
|
|
34
|
+
* writer would reintroduce it at the last possible moment.
|
|
35
|
+
*
|
|
36
|
+
* `unreached` is drawn without alarm and without apology. It is not a hole, and
|
|
37
|
+
* it is not somebody's standing decision either — it is the run having read the
|
|
38
|
+
* diff against every stored baseline and concluded the change cannot arrive.
|
|
39
|
+
*/
|
|
40
|
+
export declare function CoverageLine({ build }: {
|
|
41
|
+
readonly build: BuildSummary;
|
|
42
|
+
}): ReactElement;
|
|
43
|
+
//# sourceMappingURL=builds.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The front page: every build the store has, newest first.
|
|
4
|
+
*
|
|
5
|
+
* A list, and deliberately only a list. What a reader wants here is which build
|
|
6
|
+
* to open, and the two facts that decide it are how many subjects still have
|
|
7
|
+
* nobody's name against them and whether the run observed what it planned to.
|
|
8
|
+
* Everything else about a build is on the build.
|
|
9
|
+
*/
|
|
10
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
11
|
+
import { Failure, Go, Topbar, messageOf } from './shell.js';
|
|
12
|
+
import { number, when } from './text.js';
|
|
13
|
+
export function BuildsPage({ client, limit, go, }) {
|
|
14
|
+
const [builds, setBuilds] = useState({ state: 'loading' });
|
|
15
|
+
const load = useCallback(async () => {
|
|
16
|
+
setBuilds({ state: 'loading' });
|
|
17
|
+
try {
|
|
18
|
+
setBuilds({ state: 'ready', value: await client.builds(limit) });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
// Reported, never rendered as an empty list. "No builds need review" is
|
|
22
|
+
// the sentence somebody merges on.
|
|
23
|
+
setBuilds({ state: 'failed', why: messageOf(error) });
|
|
24
|
+
}
|
|
25
|
+
}, [client, limit]);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
void load();
|
|
28
|
+
}, [load]);
|
|
29
|
+
return (_jsxs("div", { className: "va-app", children: [_jsx(Topbar, { crumbs: [], go: go, title: "Variance Authority", subtitle: builds.state === 'ready' ? (builds.value[0]?.project ?? 'review') : 'review', children: _jsx("nav", { className: "va-nav va-topbar-meta", children: _jsx(Go, { to: { page: 'changelog' }, go: go, className: "va-mode", children: "Changelog" }) }) }), _jsx("div", { className: "va-body va-scroll", children: _jsxs("div", { className: "va-page", children: [builds.state === 'loading' ? _jsx("p", { className: "va-note", children: "Loading builds\u2026" }) : null, builds.state === 'failed' ? _jsx(Failure, { why: builds.why, retry: load }) : null, builds.state === 'ready' ? _jsx(BuildList, { builds: builds.value, go: go }) : null] }) })] }));
|
|
30
|
+
}
|
|
31
|
+
export function BuildList({ builds, go, }) {
|
|
32
|
+
if (builds.length === 0)
|
|
33
|
+
return _jsx("p", { className: "va-note", children: "No builds have been posted yet." });
|
|
34
|
+
return (_jsx("ol", { className: "va-builds", children: builds.map((build) => (_jsxs("li", { className: "va-build", children: [_jsxs("p", { className: "va-build-head", children: [_jsxs(Go, { to: { page: 'build', build: build.build }, go: go, className: "va-build-open", children: [_jsx("span", { className: "va-build-id", children: build.build }), _jsx("span", { className: "va-build-go", children: "Review \u2192" })] }), _jsx("code", { className: "va-commit", children: build.commit.slice(0, 8) }), build.branch === undefined ? null : _jsx("span", { className: "va-branch", children: build.branch }), _jsx("span", { className: "va-when", children: when(build.at) })] }), _jsx(Verdicts, { build: build }), _jsx(CoverageLine, { build: build })] }, build.build))) }));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The counts, with `pending` given the emphasis.
|
|
38
|
+
*
|
|
39
|
+
* `unchanged` is the large number and the uninteresting one. What decides whether
|
|
40
|
+
* anybody has to open this build is how many subjects still have nobody's name
|
|
41
|
+
* against them.
|
|
42
|
+
*
|
|
43
|
+
* Every verdict the store keeps is named, including `ignored`, which this line
|
|
44
|
+
* used to leave out. Four words over a five-word vocabulary is a line that does
|
|
45
|
+
* not add up to the build it describes — twenty subjects reported as eighteen —
|
|
46
|
+
* and the two it silently dropped are the two that came back green because a
|
|
47
|
+
* declaration said so, which is the half of a build worth auditing.
|
|
48
|
+
*/
|
|
49
|
+
function Verdicts({ build }) {
|
|
50
|
+
return (_jsxs("p", { className: "va-verdicts", children: [_jsx("span", { className: build.pending > 0 ? 'va-pending' : 'va-settled', children: build.pending > 0 ? `${number(build.pending)} awaiting review` : 'nothing awaiting review' }), _jsxs("span", { className: "va-counts", children: [build.verdicts.changed, " changed \u00B7 ", build.verdicts.new, " new \u00B7", ' ', build.verdicts.incomparable, " incomparable \u00B7 ", build.verdicts.ignored, " ignored \u00B7", ' ', build.verdicts.unchanged, " unchanged"] })] }));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* How much of the suite this run looked at, and what happened to the rest.
|
|
54
|
+
*
|
|
55
|
+
* **The denominator is the line.** A run that narrowed twenty subjects to two
|
|
56
|
+
* has done the most expensive thinking in the product, and a page reading
|
|
57
|
+
* `2 subjects` reports a suite of two. `2 of 20 observed` is the same fact with
|
|
58
|
+
* the eighteen still in it.
|
|
59
|
+
*
|
|
60
|
+
* The states are not two. A run that stated its coverage and skipped nothing is
|
|
61
|
+
* clean; a run that stated it and failed on fifty is not; and a run that never
|
|
62
|
+
* said is **unknown**, which must not be drawn as the first. That is the collapse
|
|
63
|
+
* `RunReport.notObserved` exists to prevent, and drawing `0 failed` for a silent
|
|
64
|
+
* writer would reintroduce it at the last possible moment.
|
|
65
|
+
*
|
|
66
|
+
* `unreached` is drawn without alarm and without apology. It is not a hole, and
|
|
67
|
+
* it is not somebody's standing decision either — it is the run having read the
|
|
68
|
+
* diff against every stored baseline and concluded the change cannot arrive.
|
|
69
|
+
*/
|
|
70
|
+
export function CoverageLine({ build }) {
|
|
71
|
+
const { coverage } = build;
|
|
72
|
+
if (!coverage.stated) {
|
|
73
|
+
return (_jsx("p", { className: "va-coverage va-unknown", children: "This report did not say which subjects it skipped, so its coverage is unknown \u2014 not clean." }));
|
|
74
|
+
}
|
|
75
|
+
const seen = Object.values(build.verdicts).reduce((sum, n) => sum + n, 0);
|
|
76
|
+
const planned = seen + coverage.failed + coverage.excluded + coverage.unreached;
|
|
77
|
+
return (_jsxs("p", { className: coverage.failed > 0 ? 'va-coverage va-incomplete' : 'va-coverage', children: [_jsx("span", { className: "va-num", children: seen }), " of ", _jsx("span", { className: "va-num", children: planned }), " observed", coverage.unreached === 0 ? null : (_jsxs(_Fragment, { children: [' · ', _jsx("span", { className: "va-num", children: coverage.unreached }), " not reached by this change"] })), coverage.failed === 0 ? null : _jsxs(_Fragment, { children: [" \u00B7 ", coverage.failed, " failed to render"] }), coverage.excluded === 0 ? null : _jsxs(_Fragment, { children: [" \u00B7 ", coverage.excluded, " excluded by configuration"] })] }));
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=builds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builds.js","sourceRoot":"","sources":["../../src/ui/builds.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * The front page: every build the store has, newest first.\n *\n * A list, and deliberately only a list. What a reader wants here is which build\n * to open, and the two facts that decide it are how many subjects still have\n * nobody's name against them and whether the run observed what it planned to.\n * Everything else about a build is on the build.\n */\n\nimport { useCallback, useEffect, useState, type ReactElement } from 'react';\nimport type { BuildSummary } from '../review-types.js';\nimport type { ReviewClient } from './client.js';\nimport type { Route } from './route.js';\nimport { Failure, Go, Topbar, messageOf, type Loaded } from './shell.js';\nimport { number, when } from './text.js';\n\nexport function BuildsPage({\n client,\n limit,\n go,\n}: {\n readonly client: ReviewClient;\n readonly limit?: number | undefined;\n readonly go: (route: Route) => void;\n}): ReactElement {\n const [builds, setBuilds] = useState<Loaded<readonly BuildSummary[]>>({ state: 'loading' });\n\n const load = useCallback(async (): Promise<void> => {\n setBuilds({ state: 'loading' });\n try {\n setBuilds({ state: 'ready', value: await client.builds(limit) });\n } catch (error) {\n // Reported, never rendered as an empty list. \"No builds need review\" is\n // the sentence somebody merges on.\n setBuilds({ state: 'failed', why: messageOf(error) });\n }\n }, [client, limit]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n return (\n <div className=\"va-app\">\n <Topbar\n crumbs={[]}\n go={go}\n title=\"Variance Authority\"\n subtitle={builds.state === 'ready' ? (builds.value[0]?.project ?? 'review') : 'review'}\n >\n <nav className=\"va-nav va-topbar-meta\">\n <Go to={{ page: 'changelog' }} go={go} className=\"va-mode\">\n Changelog\n </Go>\n </nav>\n </Topbar>\n\n <div className=\"va-body va-scroll\">\n <div className=\"va-page\">\n {builds.state === 'loading' ? <p className=\"va-note\">Loading builds…</p> : null}\n {builds.state === 'failed' ? <Failure why={builds.why} retry={load} /> : null}\n {builds.state === 'ready' ? <BuildList builds={builds.value} go={go} /> : null}\n </div>\n </div>\n </div>\n );\n}\n\nexport function BuildList({\n builds,\n go,\n}: {\n readonly builds: readonly BuildSummary[];\n readonly go: (route: Route) => void;\n}): ReactElement {\n if (builds.length === 0) return <p className=\"va-note\">No builds have been posted yet.</p>;\n\n return (\n <ol className=\"va-builds\">\n {builds.map((build) => (\n <li key={build.build} className=\"va-build\">\n <p className=\"va-build-head\">\n <Go to={{ page: 'build', build: build.build }} go={go} className=\"va-build-open\">\n <span className=\"va-build-id\">{build.build}</span>\n <span className=\"va-build-go\">Review →</span>\n </Go>\n <code className=\"va-commit\">{build.commit.slice(0, 8)}</code>\n {build.branch === undefined ? null : <span className=\"va-branch\">{build.branch}</span>}\n <span className=\"va-when\">{when(build.at)}</span>\n </p>\n <Verdicts build={build} />\n <CoverageLine build={build} />\n </li>\n ))}\n </ol>\n );\n}\n\n/**\n * The counts, with `pending` given the emphasis.\n *\n * `unchanged` is the large number and the uninteresting one. What decides whether\n * anybody has to open this build is how many subjects still have nobody's name\n * against them.\n *\n * Every verdict the store keeps is named, including `ignored`, which this line\n * used to leave out. Four words over a five-word vocabulary is a line that does\n * not add up to the build it describes — twenty subjects reported as eighteen —\n * and the two it silently dropped are the two that came back green because a\n * declaration said so, which is the half of a build worth auditing.\n */\nfunction Verdicts({ build }: { readonly build: BuildSummary }): ReactElement {\n return (\n <p className=\"va-verdicts\">\n <span className={build.pending > 0 ? 'va-pending' : 'va-settled'}>\n {build.pending > 0 ? `${number(build.pending)} awaiting review` : 'nothing awaiting review'}\n </span>\n <span className=\"va-counts\">\n {build.verdicts.changed} changed · {build.verdicts.new} new ·{' '}\n {build.verdicts.incomparable} incomparable · {build.verdicts.ignored} ignored ·{' '}\n {build.verdicts.unchanged} unchanged\n </span>\n </p>\n );\n}\n\n/**\n * How much of the suite this run looked at, and what happened to the rest.\n *\n * **The denominator is the line.** A run that narrowed twenty subjects to two\n * has done the most expensive thinking in the product, and a page reading\n * `2 subjects` reports a suite of two. `2 of 20 observed` is the same fact with\n * the eighteen still in it.\n *\n * The states are not two. A run that stated its coverage and skipped nothing is\n * clean; a run that stated it and failed on fifty is not; and a run that never\n * said is **unknown**, which must not be drawn as the first. That is the collapse\n * `RunReport.notObserved` exists to prevent, and drawing `0 failed` for a silent\n * writer would reintroduce it at the last possible moment.\n *\n * `unreached` is drawn without alarm and without apology. It is not a hole, and\n * it is not somebody's standing decision either — it is the run having read the\n * diff against every stored baseline and concluded the change cannot arrive.\n */\nexport function CoverageLine({ build }: { readonly build: BuildSummary }): ReactElement {\n const { coverage } = build;\n if (!coverage.stated) {\n return (\n <p className=\"va-coverage va-unknown\">\n This report did not say which subjects it skipped, so its coverage is unknown — not clean.\n </p>\n );\n }\n\n const seen = Object.values(build.verdicts).reduce((sum, n) => sum + n, 0);\n const planned = seen + coverage.failed + coverage.excluded + coverage.unreached;\n\n return (\n <p className={coverage.failed > 0 ? 'va-coverage va-incomplete' : 'va-coverage'}>\n <span className=\"va-num\">{seen}</span> of <span className=\"va-num\">{planned}</span> observed\n {coverage.unreached === 0 ? null : (\n <>\n {' · '}\n <span className=\"va-num\">{coverage.unreached}</span> not reached by this change\n </>\n )}\n {coverage.failed === 0 ? null : <> · {coverage.failed} failed to render</>}\n {coverage.excluded === 0 ? null : <> · {coverage.excluded} excluded by configuration</>}\n </p>\n );\n}\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The other changes riding in the same picture as this one.
|
|
3
|
+
*
|
|
4
|
+
* A decision is taken on a *subject*, and approving one promotes the whole
|
|
5
|
+
* candidate image as the new baseline. So a render that carries this change and
|
|
6
|
+
* two others settles all three, under whichever of them the reviewer happened to
|
|
7
|
+
* open. Nothing on the page said so, and the reviewer had no way to find out: the
|
|
8
|
+
* docket files each render under one change, and the other two are on their own
|
|
9
|
+
* pages with their own approve buttons that have already been pressed by proxy.
|
|
10
|
+
*
|
|
11
|
+
* At one component and a dozen renders that is a small hazard. A button drawn in
|
|
12
|
+
* three thousand shots is where it stops being one — the batch is the only way to
|
|
13
|
+
* decide it, and the batch is exactly what makes the co-carried change invisible.
|
|
14
|
+
*
|
|
15
|
+
* ## What counts as another change here
|
|
16
|
+
*
|
|
17
|
+
* `cause` on the component hashes, which is *its own content moved* rather than
|
|
18
|
+
* *its box was pushed*. A container that reflowed is not a second change and
|
|
19
|
+
* listing it would put six framework wrappers on every render. The named few that
|
|
20
|
+
* come back are the components a reviewer would have opened a page for.
|
|
21
|
+
*
|
|
22
|
+
* ## What it will not say
|
|
23
|
+
*
|
|
24
|
+
* That a render carries only this change, when nothing compared its hashes. A
|
|
25
|
+
* baseline without component digests answers *not read*, and reading that as
|
|
26
|
+
* *nothing else is here* is the reassurance this whole surface must never give.
|
|
27
|
+
*/
|
|
28
|
+
import type { ReactElement } from 'react';
|
|
29
|
+
import type { BuildDetail } from '../review-types.js';
|
|
30
|
+
import type { Route } from './route.js';
|
|
31
|
+
/** One other component causing a difference in renders this change is decided in. */
|
|
32
|
+
export interface Carried {
|
|
33
|
+
readonly component: string;
|
|
34
|
+
/** Renders of this change it also moved in, in the order they were given. */
|
|
35
|
+
readonly subjects: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
/** What else is in the pictures a decision here would promote. */
|
|
38
|
+
export interface Carrying {
|
|
39
|
+
/** The other causes, widest first. */
|
|
40
|
+
readonly others: readonly Carried[];
|
|
41
|
+
/** Renders whose hashes were read and name no other cause. */
|
|
42
|
+
readonly alone: readonly string[];
|
|
43
|
+
/** Renders whose hashes were read and name at least one. */
|
|
44
|
+
readonly shared: readonly string[];
|
|
45
|
+
/** Renders where nothing compared hashes, which is neither of the above. */
|
|
46
|
+
readonly unread: readonly string[];
|
|
47
|
+
/** The other causes in one render. */
|
|
48
|
+
readonly at: (subject: string) => readonly string[];
|
|
49
|
+
}
|
|
50
|
+
export declare function carriedWith(build: BuildDetail, component: string, subjects: readonly string[]): Carrying;
|
|
51
|
+
/**
|
|
52
|
+
* What a press of Approve settles beyond the change it is under.
|
|
53
|
+
*
|
|
54
|
+
* The button says *Approve this change (7)* and promotes seven whole pictures.
|
|
55
|
+
* Four of those seven also hold a `CardFooter` that moved on its own, and it has
|
|
56
|
+
* a page and an approve button of its own that nobody pressed — so the reviewer
|
|
57
|
+
* decides it here, under a heading with another component's name on it, and the
|
|
58
|
+
* `CardFooter` page afterwards reports renders already baselined against a
|
|
59
|
+
* decision it never recorded.
|
|
60
|
+
*
|
|
61
|
+
* Named rather than counted, and linked: the instruction the sentence implies is
|
|
62
|
+
* *go and look at that one first*, and it is one click away.
|
|
63
|
+
*/
|
|
64
|
+
export declare function AlsoCarries({ carrying, renders, build, changes, go, }: {
|
|
65
|
+
readonly carrying: Carrying;
|
|
66
|
+
/** Renders on this page, which the shared count is a fraction of. */
|
|
67
|
+
readonly renders: number;
|
|
68
|
+
readonly build: string;
|
|
69
|
+
/** Components with a change page of their own, so a link goes somewhere. */
|
|
70
|
+
readonly changes: ReadonlySet<string>;
|
|
71
|
+
readonly go: (route: Route) => void;
|
|
72
|
+
}): ReactElement | null;
|
|
73
|
+
//# sourceMappingURL=carried.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Go } from './shell.js';
|
|
3
|
+
import { count, number } from './text.js';
|
|
4
|
+
export function carriedWith(build, component, subjects) {
|
|
5
|
+
const read = new Map(build.subjects.map((each) => [each.subject, each.moved]));
|
|
6
|
+
const others = new Map();
|
|
7
|
+
const alone = [];
|
|
8
|
+
const shared = [];
|
|
9
|
+
const unread = [];
|
|
10
|
+
const at = new Map();
|
|
11
|
+
for (const subject of subjects) {
|
|
12
|
+
const moved = read.get(subject);
|
|
13
|
+
if (moved === undefined) {
|
|
14
|
+
unread.push(subject);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const causes = moved
|
|
18
|
+
.filter((each) => each.cause && each.component !== component)
|
|
19
|
+
.map((each) => each.component)
|
|
20
|
+
.sort();
|
|
21
|
+
at.set(subject, causes);
|
|
22
|
+
if (causes.length === 0)
|
|
23
|
+
alone.push(subject);
|
|
24
|
+
else
|
|
25
|
+
shared.push(subject);
|
|
26
|
+
for (const name of causes) {
|
|
27
|
+
const seen = others.get(name);
|
|
28
|
+
if (seen === undefined)
|
|
29
|
+
others.set(name, [subject]);
|
|
30
|
+
else
|
|
31
|
+
seen.push(subject);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
others: [...others.entries()]
|
|
36
|
+
.map(([name, where]) => ({ component: name, subjects: where }))
|
|
37
|
+
.sort((left, right) => right.subjects.length - left.subjects.length ||
|
|
38
|
+
left.component.localeCompare(right.component)),
|
|
39
|
+
alone,
|
|
40
|
+
shared,
|
|
41
|
+
unread,
|
|
42
|
+
at: (subject) => at.get(subject) ?? [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* What a press of Approve settles beyond the change it is under.
|
|
47
|
+
*
|
|
48
|
+
* The button says *Approve this change (7)* and promotes seven whole pictures.
|
|
49
|
+
* Four of those seven also hold a `CardFooter` that moved on its own, and it has
|
|
50
|
+
* a page and an approve button of its own that nobody pressed — so the reviewer
|
|
51
|
+
* decides it here, under a heading with another component's name on it, and the
|
|
52
|
+
* `CardFooter` page afterwards reports renders already baselined against a
|
|
53
|
+
* decision it never recorded.
|
|
54
|
+
*
|
|
55
|
+
* Named rather than counted, and linked: the instruction the sentence implies is
|
|
56
|
+
* *go and look at that one first*, and it is one click away.
|
|
57
|
+
*/
|
|
58
|
+
export function AlsoCarries({ carrying, renders, build, changes, go, }) {
|
|
59
|
+
if (carrying.others.length === 0 && carrying.unread.length === 0)
|
|
60
|
+
return null;
|
|
61
|
+
return (_jsxs("p", { className: "va-carries", children: [carrying.others.length === 0 ? null : (_jsxs(_Fragment, { children: [_jsxs("strong", { children: [number(carrying.shared.length), " of ", count(renders, 'render')] }), ' ', "also carry", ' ', carrying.others.map((other, index) => (_jsxs("span", { children: [index === 0 ? '' : index === carrying.others.length - 1 ? ' and ' : ', ', changes.has(other.component) ? (_jsx(Go, { to: { page: 'change', build, change: other.component }, go: go, className: "va-carries-go", children: other.component })) : (_jsx("strong", { children: other.component })), _jsxs("span", { className: "va-note", children: [" (", number(other.subjects.length), ")"] })] }, other.component))), ". A decision is taken on the whole picture, so approving here accepts those in the same renders \u2014 before anyone opened their page.", ' '] })), carrying.unread.length === 0 ? null : (_jsxs("span", { className: "va-note", children: [count(carrying.unread.length, 'render'), " compared against a baseline with no component hashes, so nothing says what else is in ", carrying.unread.length === 1 ? 'it' : 'them', "."] }))] }));
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=carried.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"carried.js","sourceRoot":"","sources":["../../src/ui/carried.tsx"],"names":[],"mappings":";AA+BA,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AAChC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAuB1C,MAAM,UAAU,WAAW,CACzB,KAAkB,EAClB,SAAiB,EACjB,QAA2B;IAE3B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC/E,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,EAAE,GAAG,IAAI,GAAG,EAA6B,CAAC;IAEhD,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACrB,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,KAAK;aACjB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC;aAC5D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;aAC7B,IAAI,EAAE,CAAC;QAEV,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACxB,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;;YACxC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1B,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,IAAI,KAAK,SAAS;gBAAE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;;gBAC/C,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,OAAO;QACL,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;aAC1B,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;aAC9D,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM;YACjE,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAClD,KAAK;QACL,MAAM;QACN,MAAM;QACN,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE;KACvC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,QAAQ,EACR,OAAO,EACP,KAAK,EACL,OAAO,EACP,EAAE,GASH;IACC,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE9E,OAAO,CACL,aAAG,SAAS,EAAC,YAAY,aACtB,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACrC,8BACE,6BACG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,UAAM,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,IACtD,EAAC,GAAG,gBACF,GAAG,EACb,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CACrC,2BACG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EACxE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAC9B,KAAC,EAAE,IACD,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,SAAS,EAAE,EACtD,EAAE,EAAE,EAAE,EACN,SAAS,EAAC,eAAe,YAExB,KAAK,CAAC,SAAS,GACb,CACN,CAAC,CAAC,CAAC,CACF,2BAAS,KAAK,CAAC,SAAS,GAAU,CACnC,EACD,gBAAM,SAAS,EAAC,SAAS,mBAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,KAb1D,KAAK,CAAC,SAAS,CAcnB,CACR,CAAC,6IAEyC,GAAG,IAC7C,CACJ,EACA,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACrC,gBAAM,SAAS,EAAC,SAAS,aACtB,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,6FACC,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,SAChF,CACR,IACC,CACL,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The other changes riding in the same picture as this one.\n *\n * A decision is taken on a *subject*, and approving one promotes the whole\n * candidate image as the new baseline. So a render that carries this change and\n * two others settles all three, under whichever of them the reviewer happened to\n * open. Nothing on the page said so, and the reviewer had no way to find out: the\n * docket files each render under one change, and the other two are on their own\n * pages with their own approve buttons that have already been pressed by proxy.\n *\n * At one component and a dozen renders that is a small hazard. A button drawn in\n * three thousand shots is where it stops being one — the batch is the only way to\n * decide it, and the batch is exactly what makes the co-carried change invisible.\n *\n * ## What counts as another change here\n *\n * `cause` on the component hashes, which is *its own content moved* rather than\n * *its box was pushed*. A container that reflowed is not a second change and\n * listing it would put six framework wrappers on every render. The named few that\n * come back are the components a reviewer would have opened a page for.\n *\n * ## What it will not say\n *\n * That a render carries only this change, when nothing compared its hashes. A\n * baseline without component digests answers *not read*, and reading that as\n * *nothing else is here* is the reassurance this whole surface must never give.\n */\n\nimport type { ReactElement } from 'react';\nimport type { BuildDetail } from '../review-types.js';\nimport type { Route } from './route.js';\nimport { Go } from './shell.js';\nimport { count, number } from './text.js';\n\n/** One other component causing a difference in renders this change is decided in. */\nexport interface Carried {\n readonly component: string;\n /** Renders of this change it also moved in, in the order they were given. */\n readonly subjects: readonly string[];\n}\n\n/** What else is in the pictures a decision here would promote. */\nexport interface Carrying {\n /** The other causes, widest first. */\n readonly others: readonly Carried[];\n /** Renders whose hashes were read and name no other cause. */\n readonly alone: readonly string[];\n /** Renders whose hashes were read and name at least one. */\n readonly shared: readonly string[];\n /** Renders where nothing compared hashes, which is neither of the above. */\n readonly unread: readonly string[];\n /** The other causes in one render. */\n readonly at: (subject: string) => readonly string[];\n}\n\nexport function carriedWith(\n build: BuildDetail,\n component: string,\n subjects: readonly string[],\n): Carrying {\n const read = new Map(build.subjects.map((each) => [each.subject, each.moved]));\n const others = new Map<string, string[]>();\n const alone: string[] = [];\n const shared: string[] = [];\n const unread: string[] = [];\n const at = new Map<string, readonly string[]>();\n\n for (const subject of subjects) {\n const moved = read.get(subject);\n if (moved === undefined) {\n unread.push(subject);\n continue;\n }\n\n const causes = moved\n .filter((each) => each.cause && each.component !== component)\n .map((each) => each.component)\n .sort();\n\n at.set(subject, causes);\n if (causes.length === 0) alone.push(subject);\n else shared.push(subject);\n for (const name of causes) {\n const seen = others.get(name);\n if (seen === undefined) others.set(name, [subject]);\n else seen.push(subject);\n }\n }\n\n return {\n others: [...others.entries()]\n .map(([name, where]) => ({ component: name, subjects: where }))\n .sort((left, right) => right.subjects.length - left.subjects.length ||\n left.component.localeCompare(right.component)),\n alone,\n shared,\n unread,\n at: (subject) => at.get(subject) ?? [],\n };\n}\n\n/**\n * What a press of Approve settles beyond the change it is under.\n *\n * The button says *Approve this change (7)* and promotes seven whole pictures.\n * Four of those seven also hold a `CardFooter` that moved on its own, and it has\n * a page and an approve button of its own that nobody pressed — so the reviewer\n * decides it here, under a heading with another component's name on it, and the\n * `CardFooter` page afterwards reports renders already baselined against a\n * decision it never recorded.\n *\n * Named rather than counted, and linked: the instruction the sentence implies is\n * *go and look at that one first*, and it is one click away.\n */\nexport function AlsoCarries({\n carrying,\n renders,\n build,\n changes,\n go,\n}: {\n readonly carrying: Carrying;\n /** Renders on this page, which the shared count is a fraction of. */\n readonly renders: number;\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 (carrying.others.length === 0 && carrying.unread.length === 0) return null;\n\n return (\n <p className=\"va-carries\">\n {carrying.others.length === 0 ? null : (\n <>\n <strong>\n {number(carrying.shared.length)} of {count(renders, 'render')}\n </strong>{' '}\n also carry{' '}\n {carrying.others.map((other, index) => (\n <span key={other.component}>\n {index === 0 ? '' : index === carrying.others.length - 1 ? ' and ' : ', '}\n {changes.has(other.component) ? (\n <Go\n to={{ page: 'change', build, change: other.component }}\n go={go}\n className=\"va-carries-go\"\n >\n {other.component}\n </Go>\n ) : (\n <strong>{other.component}</strong>\n )}\n <span className=\"va-note\"> ({number(other.subjects.length)})</span>\n </span>\n ))}\n . A decision is taken on the whole picture, so approving here accepts those in the same\n renders — before anyone opened their page.{' '}\n </>\n )}\n {carrying.unread.length === 0 ? null : (\n <span className=\"va-note\">\n {count(carrying.unread.length, 'render')} compared against a baseline with no component\n hashes, so nothing says what else is in {carrying.unread.length === 1 ? 'it' : 'them'}.\n </span>\n )}\n </p>\n );\n}\n"]}
|