@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,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each change hangs from — the file, or the property, that the commit changed.
|
|
3
|
+
*
|
|
4
|
+
* The docket grouped changes by *rung*: everything the diff declares under
|
|
5
|
+
* **Edited**, everything an edited parent hands its inputs to under **Owner
|
|
6
|
+
* edited**. That is the run's ladder printed as an index, and it is a category a
|
|
7
|
+
* reviewer never has. Nobody opens a build to work through the components whose
|
|
8
|
+
* cause is one hop away. They edited three files and they want to know what each
|
|
9
|
+
* of the three did.
|
|
10
|
+
*
|
|
11
|
+
* So the row is the cause. `Button` moved because `ui/button.tsx` changed;
|
|
12
|
+
* `CardFooter` moved because `ProductCard.tsx` changed and `ProductCard` draws it
|
|
13
|
+
* through `Card`. Two rungs, one root each, and the roots are the commit —
|
|
14
|
+
* three files in, three headings out, and a reviewer who reads them has read
|
|
15
|
+
* their own change rather than a taxonomy of it.
|
|
16
|
+
*
|
|
17
|
+
* ## Why the root is a file the commit changed, and never a declaring file
|
|
18
|
+
*
|
|
19
|
+
* An owner is a root because the commit **edited** it. The file that *declares*
|
|
20
|
+
* `MainNav` is the answer to a different question, and the two paths are the same
|
|
21
|
+
* only when the commit happened to edit that file directly. Rooting under a
|
|
22
|
+
* declaration the commit never touched would open a heading nobody wrote — the
|
|
23
|
+
* page inventing a cause out of the source index — so a name whose file is not in
|
|
24
|
+
* the change set gets a root under its own name and says so.
|
|
25
|
+
*
|
|
26
|
+
* ## An origin can hang from two roots, and it hangs from both
|
|
27
|
+
*
|
|
28
|
+
* The same `Button` is `edited` on the page whose file the diff names and
|
|
29
|
+
* `upstream` on the page where a changed parent hands it a different label. That
|
|
30
|
+
* is two causes for one component, and picking one would file a render under a
|
|
31
|
+
* commit that did not produce it. It is listed under each, carrying the count of
|
|
32
|
+
* renders that root explains — which is also the shape that keeps a partial
|
|
33
|
+
* answer from reading as a whole one.
|
|
34
|
+
*/
|
|
35
|
+
import type { BuildDetail } from '../review-types.js';
|
|
36
|
+
import type { Origin } from './grouping.js';
|
|
37
|
+
/** What kind of thing a root is, which decides how a heading draws it. */
|
|
38
|
+
export type RootKind = 'file' | 'token' | 'component';
|
|
39
|
+
/** One change under a root, and how much of it the root accounts for. */
|
|
40
|
+
export interface Rooted {
|
|
41
|
+
readonly origin: Origin;
|
|
42
|
+
/**
|
|
43
|
+
* Renders this root explains, when it is not all of them.
|
|
44
|
+
*
|
|
45
|
+
* Absent is *all of them*, and it is absent rather than equal to the length so
|
|
46
|
+
* that a row printing it has nothing to print in the ordinary case. Present
|
|
47
|
+
* means the component moved for a second reason somewhere else, and the
|
|
48
|
+
* heading above it accounts for this many of its renders and no more.
|
|
49
|
+
*/
|
|
50
|
+
readonly of?: number;
|
|
51
|
+
}
|
|
52
|
+
/** A cause in the commit, and everything that moved because of it. */
|
|
53
|
+
export interface Root {
|
|
54
|
+
readonly kind: RootKind;
|
|
55
|
+
/** The path, the custom property, or the component name. */
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly changes: readonly Rooted[];
|
|
58
|
+
/** Renders under it, summed over the changes. */
|
|
59
|
+
readonly renders: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The commit's causes, and the changes no cause in it accounts for.
|
|
63
|
+
*
|
|
64
|
+
* `loose` is not a failure and is not small. It holds every change on a rung with
|
|
65
|
+
* nothing in the commit at the top of it — nothing reaches it, nothing explains
|
|
66
|
+
* it, the run read no diff — and those are the findings the docket ranks above
|
|
67
|
+
* everything here. They keep their own bands.
|
|
68
|
+
*/
|
|
69
|
+
export declare function rootsOf(origins: readonly Origin[], build: BuildDetail): {
|
|
70
|
+
readonly roots: readonly Root[];
|
|
71
|
+
readonly loose: readonly Origin[];
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=cause.d.ts.map
|
package/dist/ui/cause.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each change hangs from — the file, or the property, that the commit changed.
|
|
3
|
+
*
|
|
4
|
+
* The docket grouped changes by *rung*: everything the diff declares under
|
|
5
|
+
* **Edited**, everything an edited parent hands its inputs to under **Owner
|
|
6
|
+
* edited**. That is the run's ladder printed as an index, and it is a category a
|
|
7
|
+
* reviewer never has. Nobody opens a build to work through the components whose
|
|
8
|
+
* cause is one hop away. They edited three files and they want to know what each
|
|
9
|
+
* of the three did.
|
|
10
|
+
*
|
|
11
|
+
* So the row is the cause. `Button` moved because `ui/button.tsx` changed;
|
|
12
|
+
* `CardFooter` moved because `ProductCard.tsx` changed and `ProductCard` draws it
|
|
13
|
+
* through `Card`. Two rungs, one root each, and the roots are the commit —
|
|
14
|
+
* three files in, three headings out, and a reviewer who reads them has read
|
|
15
|
+
* their own change rather than a taxonomy of it.
|
|
16
|
+
*
|
|
17
|
+
* ## Why the root is a file the commit changed, and never a declaring file
|
|
18
|
+
*
|
|
19
|
+
* An owner is a root because the commit **edited** it. The file that *declares*
|
|
20
|
+
* `MainNav` is the answer to a different question, and the two paths are the same
|
|
21
|
+
* only when the commit happened to edit that file directly. Rooting under a
|
|
22
|
+
* declaration the commit never touched would open a heading nobody wrote — the
|
|
23
|
+
* page inventing a cause out of the source index — so a name whose file is not in
|
|
24
|
+
* the change set gets a root under its own name and says so.
|
|
25
|
+
*
|
|
26
|
+
* ## An origin can hang from two roots, and it hangs from both
|
|
27
|
+
*
|
|
28
|
+
* The same `Button` is `edited` on the page whose file the diff names and
|
|
29
|
+
* `upstream` on the page where a changed parent hands it a different label. That
|
|
30
|
+
* is two causes for one component, and picking one would file a render under a
|
|
31
|
+
* commit that did not produce it. It is listed under each, carrying the count of
|
|
32
|
+
* renders that root explains — which is also the shape that keeps a partial
|
|
33
|
+
* answer from reading as a whole one.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* The commit's causes, and the changes no cause in it accounts for.
|
|
37
|
+
*
|
|
38
|
+
* `loose` is not a failure and is not small. It holds every change on a rung with
|
|
39
|
+
* nothing in the commit at the top of it — nothing reaches it, nothing explains
|
|
40
|
+
* it, the run read no diff — and those are the findings the docket ranks above
|
|
41
|
+
* everything here. They keep their own bands.
|
|
42
|
+
*/
|
|
43
|
+
export function rootsOf(origins, build) {
|
|
44
|
+
const changedFiles = filesInCommit(build);
|
|
45
|
+
const found = new Map();
|
|
46
|
+
const loose = [];
|
|
47
|
+
for (const origin of origins) {
|
|
48
|
+
const mine = new Map();
|
|
49
|
+
for (const { movement } of origin.appearances) {
|
|
50
|
+
for (const at of rootsFor(movement, changedFiles)) {
|
|
51
|
+
mine.set(at, (mine.get(at) ?? 0) + 1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (mine.size === 0) {
|
|
55
|
+
loose.push(origin);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
for (const [at, renders] of mine) {
|
|
59
|
+
const [kind, name] = split(at);
|
|
60
|
+
const root = found.get(at) ?? { kind, name, changes: [] };
|
|
61
|
+
root.changes.push({
|
|
62
|
+
origin,
|
|
63
|
+
...(renders === origin.appearances.length ? {} : { of: renders }),
|
|
64
|
+
});
|
|
65
|
+
found.set(at, root);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const roots = [...found.values()]
|
|
69
|
+
.map((root) => ({
|
|
70
|
+
...root,
|
|
71
|
+
changes: [...root.changes].sort((left, right) => left.origin.component.localeCompare(right.origin.component)),
|
|
72
|
+
renders: root.changes.reduce((total, { origin, of }) => total + (of ?? origin.appearances.length), 0),
|
|
73
|
+
}))
|
|
74
|
+
.sort((left, right) => RANK.indexOf(left.kind) - RANK.indexOf(right.kind) ||
|
|
75
|
+
left.name.localeCompare(right.name));
|
|
76
|
+
return { roots, loose };
|
|
77
|
+
}
|
|
78
|
+
/** Files first, then properties, then the owners whose file the commit misses. */
|
|
79
|
+
const RANK = ['file', 'token', 'component'];
|
|
80
|
+
function rootsFor(movement, changed) {
|
|
81
|
+
if (movement === undefined)
|
|
82
|
+
return [];
|
|
83
|
+
switch (movement.cause) {
|
|
84
|
+
case 'edited':
|
|
85
|
+
return movement.file === undefined ? [] : [at('file', movement.file)];
|
|
86
|
+
case 'token':
|
|
87
|
+
return (movement.tokens ?? []).map((token) => at('token', token));
|
|
88
|
+
case 'upstream': {
|
|
89
|
+
const owner = movement.upstream;
|
|
90
|
+
if (owner === undefined)
|
|
91
|
+
return [];
|
|
92
|
+
const file = changed.get(owner);
|
|
93
|
+
return [file === undefined ? at('component', owner) : at('file', file)];
|
|
94
|
+
}
|
|
95
|
+
default:
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Each component the commit reaches, mapped to the changed file it is reached by.
|
|
101
|
+
*
|
|
102
|
+
* The reach trail's head is a path the commit changed, which is the one this
|
|
103
|
+
* needs. A component's own declaration wins when the commit changed that file
|
|
104
|
+
* too, because a heading a reviewer recognises as *the file I edited* beats the
|
|
105
|
+
* import at the top of the chain that led to it — and when the commit did not
|
|
106
|
+
* change the declaring file, that path is not a cause and is not offered as one.
|
|
107
|
+
*/
|
|
108
|
+
function filesInCommit(build) {
|
|
109
|
+
const changed = new Set(build.reach?.changed ?? []);
|
|
110
|
+
const found = new Map();
|
|
111
|
+
for (const entry of build.reach?.components ?? []) {
|
|
112
|
+
const head = entry.trail[0];
|
|
113
|
+
if (head !== undefined && changed.has(head))
|
|
114
|
+
found.set(entry.component, head);
|
|
115
|
+
}
|
|
116
|
+
for (const cause of build.causes) {
|
|
117
|
+
if (cause.file !== undefined && changed.has(cause.file))
|
|
118
|
+
found.set(cause.component, cause.file);
|
|
119
|
+
}
|
|
120
|
+
return found;
|
|
121
|
+
}
|
|
122
|
+
// The same separator, and for the same reason, as `attribution.ts`: a kind and a
|
|
123
|
+
// name joined by anything either half can contain is two roots that can collide.
|
|
124
|
+
// Written as the escape rather than as the byte, so the file stays greppable.
|
|
125
|
+
const IN = '\u0000';
|
|
126
|
+
function at(kind, name) {
|
|
127
|
+
return `${kind}${IN}${name}`;
|
|
128
|
+
}
|
|
129
|
+
function split(key) {
|
|
130
|
+
const cut = key.indexOf(IN);
|
|
131
|
+
return [key.slice(0, cut), key.slice(cut + 1)];
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=cause.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cause.js","sourceRoot":"","sources":["../../src/ui/cause.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What each change hangs from — the file, or the property, that the commit changed.\n *\n * The docket grouped changes by *rung*: everything the diff declares under\n * **Edited**, everything an edited parent hands its inputs to under **Owner\n * edited**. That is the run's ladder printed as an index, and it is a category a\n * reviewer never has. Nobody opens a build to work through the components whose\n * cause is one hop away. They edited three files and they want to know what each\n * of the three did.\n *\n * So the row is the cause. `Button` moved because `ui/button.tsx` changed;\n * `CardFooter` moved because `ProductCard.tsx` changed and `ProductCard` draws it\n * through `Card`. Two rungs, one root each, and the roots are the commit —\n * three files in, three headings out, and a reviewer who reads them has read\n * their own change rather than a taxonomy of it.\n *\n * ## Why the root is a file the commit changed, and never a declaring file\n *\n * An owner is a root because the commit **edited** it. The file that *declares*\n * `MainNav` is the answer to a different question, and the two paths are the same\n * only when the commit happened to edit that file directly. Rooting under a\n * declaration the commit never touched would open a heading nobody wrote — the\n * page inventing a cause out of the source index — so a name whose file is not in\n * the change set gets a root under its own name and says so.\n *\n * ## An origin can hang from two roots, and it hangs from both\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. That\n * is two causes for one component, and picking one would file a render under a\n * commit that did not produce it. It is listed under each, carrying the count of\n * renders that root explains — which is also the shape that keeps a partial\n * answer from reading as a whole one.\n */\n\nimport type { BuildDetail, MovementView } from '../review-types.js';\nimport type { Origin } from './grouping.js';\n\n/** What kind of thing a root is, which decides how a heading draws it. */\nexport type RootKind = 'file' | 'token' | 'component';\n\n/** One change under a root, and how much of it the root accounts for. */\nexport interface Rooted {\n readonly origin: Origin;\n /**\n * Renders this root explains, when it is not all of them.\n *\n * Absent is *all of them*, and it is absent rather than equal to the length so\n * that a row printing it has nothing to print in the ordinary case. Present\n * means the component moved for a second reason somewhere else, and the\n * heading above it accounts for this many of its renders and no more.\n */\n readonly of?: number;\n}\n\n/** A cause in the commit, and everything that moved because of it. */\nexport interface Root {\n readonly kind: RootKind;\n /** The path, the custom property, or the component name. */\n readonly name: string;\n readonly changes: readonly Rooted[];\n /** Renders under it, summed over the changes. */\n readonly renders: number;\n}\n\n/**\n * The commit's causes, and the changes no cause in it accounts for.\n *\n * `loose` is not a failure and is not small. It holds every change on a rung with\n * nothing in the commit at the top of it — nothing reaches it, nothing explains\n * it, the run read no diff — and those are the findings the docket ranks above\n * everything here. They keep their own bands.\n */\nexport function rootsOf(\n origins: readonly Origin[],\n build: BuildDetail,\n): { readonly roots: readonly Root[]; readonly loose: readonly Origin[] } {\n const changedFiles = filesInCommit(build);\n const found = new Map<string, { kind: RootKind; name: string; changes: Rooted[] }>();\n const loose: Origin[] = [];\n\n for (const origin of origins) {\n const mine = new Map<string, number>();\n for (const { movement } of origin.appearances) {\n for (const at of rootsFor(movement, changedFiles)) {\n mine.set(at, (mine.get(at) ?? 0) + 1);\n }\n }\n\n if (mine.size === 0) {\n loose.push(origin);\n continue;\n }\n\n for (const [at, renders] of mine) {\n const [kind, name] = split(at);\n const root = found.get(at) ?? { kind, name, changes: [] };\n root.changes.push({\n origin,\n ...(renders === origin.appearances.length ? {} : { of: renders }),\n });\n found.set(at, root);\n }\n }\n\n const roots = [...found.values()]\n .map((root) => ({\n ...root,\n changes: [...root.changes].sort((left, right) =>\n left.origin.component.localeCompare(right.origin.component),\n ),\n renders: root.changes.reduce(\n (total, { origin, of }) => total + (of ?? origin.appearances.length),\n 0,\n ),\n }))\n .sort(\n (left, right) => RANK.indexOf(left.kind) - RANK.indexOf(right.kind) ||\n left.name.localeCompare(right.name),\n );\n\n return { roots, loose };\n}\n\n/** Files first, then properties, then the owners whose file the commit misses. */\nconst RANK: readonly RootKind[] = ['file', 'token', 'component'];\n\nfunction rootsFor(\n movement: MovementView | undefined,\n changed: ReadonlyMap<string, string>,\n): readonly string[] {\n if (movement === undefined) return [];\n\n switch (movement.cause) {\n case 'edited':\n return movement.file === undefined ? [] : [at('file', movement.file)];\n case 'token':\n return (movement.tokens ?? []).map((token) => at('token', token));\n case 'upstream': {\n const owner = movement.upstream;\n if (owner === undefined) return [];\n const file = changed.get(owner);\n return [file === undefined ? at('component', owner) : at('file', file)];\n }\n default:\n return [];\n }\n}\n\n/**\n * Each component the commit reaches, mapped to the changed file it is reached by.\n *\n * The reach trail's head is a path the commit changed, which is the one this\n * needs. A component's own declaration wins when the commit changed that file\n * too, because a heading a reviewer recognises as *the file I edited* beats the\n * import at the top of the chain that led to it — and when the commit did not\n * change the declaring file, that path is not a cause and is not offered as one.\n */\nfunction filesInCommit(build: BuildDetail): ReadonlyMap<string, string> {\n const changed = new Set(build.reach?.changed ?? []);\n const found = new Map<string, string>();\n\n for (const entry of build.reach?.components ?? []) {\n const head = entry.trail[0];\n if (head !== undefined && changed.has(head)) found.set(entry.component, head);\n }\n for (const cause of build.causes) {\n if (cause.file !== undefined && changed.has(cause.file)) found.set(cause.component, cause.file);\n }\n\n return found;\n}\n\n// The same separator, and for the same reason, as `attribution.ts`: a kind and a\n// name joined by anything either half can contain is two roots that can collide.\n// Written as the escape rather than as the byte, so the file stays greppable.\nconst IN = '\\u0000';\n\nfunction at(kind: RootKind, name: string): string {\n return `${kind}${IN}${name}`;\n}\n\nfunction split(key: string): [RootKind, string] {\n const cut = key.indexOf(IN);\n return [key.slice(0, cut) as RootKind, key.slice(cut + 1)];\n}\n"]}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the record says about one change, before anybody decides it.
|
|
3
|
+
*
|
|
4
|
+
* Seven readings, and not one of them is an act. [`change.tsx`](./change.tsx)
|
|
5
|
+
* holds the page and the two buttons; this holds every sentence printed above
|
|
6
|
+
* them, which is the half that has to be argued rather than wired — what counts
|
|
7
|
+
* as *reached*, what a silence is allowed to imply, and which of two absences a
|
|
8
|
+
* reviewer is looking at. Splitting them keeps the argument readable and stops
|
|
9
|
+
* the page that carries the decision from being mostly prose about it.
|
|
10
|
+
*
|
|
11
|
+
* The rule they share: absent is not empty. A run that read no diff, a component
|
|
12
|
+
* the source index does not declare, a render nobody wrote a reach row for and a
|
|
13
|
+
* build that counted no collateral each get their own sentence or no sentence at
|
|
14
|
+
* all, because the alternative is spending this product’s loudest line on its
|
|
15
|
+
* most ordinary fact and training the reader out of it.
|
|
16
|
+
*/
|
|
17
|
+
import { type ReactElement } from 'react';
|
|
18
|
+
import type { BuildDetail } from '../review-types.js';
|
|
19
|
+
import type { ReviewClient } from './client.js';
|
|
20
|
+
import type { Crossing } from './crossing.js';
|
|
21
|
+
import { type Origin } from './grouping.js';
|
|
22
|
+
/**
|
|
23
|
+
* Three counts, because the reviewer asks three questions and they differ.
|
|
24
|
+
*
|
|
25
|
+
* *2 distinct differences, in 7 renders* is two of them run together, and the
|
|
26
|
+
* third — how many of those a press of the button actually settles — was in the
|
|
27
|
+
* button's own label, at the other end of the header. The three come apart on
|
|
28
|
+
* every change of any size: a component moves in twelve renders, five of which
|
|
29
|
+
* the docket filed under somebody else; the twelve carry two distinct shapes, so
|
|
30
|
+
* there are two pictures to look at rather than twelve; and seven are open here,
|
|
31
|
+
* because the rest are decided or belong to the other change.
|
|
32
|
+
*
|
|
33
|
+
* Kept as three cells with the reviewer's own words on them rather than folded
|
|
34
|
+
* into a sentence. A sentence has to pick an order and an emphasis, and these
|
|
35
|
+
* are three answers of equal standing.
|
|
36
|
+
*
|
|
37
|
+
* The pixel total rides along, muted. It is the least useful figure available —
|
|
38
|
+
* one number for every size of change — and it is the one somebody eventually
|
|
39
|
+
* asks for.
|
|
40
|
+
*/
|
|
41
|
+
export declare function Tally({ origin, elsewhere, open, }: {
|
|
42
|
+
readonly origin: Origin;
|
|
43
|
+
/** Renders it moved in that another change owns, and is decided under. */
|
|
44
|
+
readonly elsewhere: number;
|
|
45
|
+
/** Renders here a press settles: undecided, and approvable. */
|
|
46
|
+
readonly open: number;
|
|
47
|
+
}): ReactElement;
|
|
48
|
+
/**
|
|
49
|
+
* Where this component is declared, and what it means when nothing says.
|
|
50
|
+
*
|
|
51
|
+
* A blank is the one answer this cell must not give. The reviewer's next move on
|
|
52
|
+
* a change they do not recognise is to open the file, and *nothing here* reads as
|
|
53
|
+
* a defect in the tool — when the ordinary cause is that the name belongs to a
|
|
54
|
+
* dependency, which the source index scans no part of and never claimed to. So
|
|
55
|
+
* the two silences are separated: a run that resolved files for other components
|
|
56
|
+
* has said something about this one, and a run that resolved none has not.
|
|
57
|
+
*/
|
|
58
|
+
export declare function Declared({ file, sourced, }: {
|
|
59
|
+
readonly file?: string | undefined;
|
|
60
|
+
readonly sourced: boolean;
|
|
61
|
+
}): ReactElement;
|
|
62
|
+
/**
|
|
63
|
+
* Whether the commit arrives here, said in five states rather than two.
|
|
64
|
+
*
|
|
65
|
+
* A run with no diff read cannot say, and saying nothing is the only honest
|
|
66
|
+
* version of that. Silence would be indistinguishable from *reached*, which is
|
|
67
|
+
* the assumption a reviewer makes by default and the one that costs them.
|
|
68
|
+
*
|
|
69
|
+
* The last of the five is now conditional: it is a reconstruction, and it stands
|
|
70
|
+
* down where the run recorded a conclusion of its own. See the branch below.
|
|
71
|
+
*
|
|
72
|
+
* The rest is the split this card got wrong twice. `reached` is computed against
|
|
73
|
+
* the components the diff can arrive at, so a component the graph carries nowhere
|
|
74
|
+
* — anything out of `node_modules`, which is most of the host nodes on a real
|
|
75
|
+
* page — can never be in that set whatever the commit did. And a render the reach
|
|
76
|
+
* record has no row for is not a render the commit fails to reach; it is a render
|
|
77
|
+
* nobody wrote down. Printing *nothing reaches it and it moved anyway* over
|
|
78
|
+
* either of those spends the loudest sentence on the page on the most ordinary
|
|
79
|
+
* fact about it, and by the time a real orphan appears the sentence has been
|
|
80
|
+
* trained out of the reader.
|
|
81
|
+
*/
|
|
82
|
+
export declare function Arrival({ origin }: {
|
|
83
|
+
readonly origin: Origin;
|
|
84
|
+
}): ReactElement | null;
|
|
85
|
+
/**
|
|
86
|
+
* What the run before this one had to say about these renders.
|
|
87
|
+
*
|
|
88
|
+
* The sentence a reviewer skips work on, and the reason it is here rather than in
|
|
89
|
+
* the crossing panel: *build 5 already showed you this, and nobody decided it* is
|
|
90
|
+
* worth reading beside the change it is about and worth nothing at the bottom of
|
|
91
|
+
* a page about twenty other subjects. The undecided count is the part that is
|
|
92
|
+
* never printed anywhere else — it is the docket being delivered twice.
|
|
93
|
+
*/
|
|
94
|
+
export declare function SinceLast({ crossing, origin, }: {
|
|
95
|
+
readonly crossing: Crossing;
|
|
96
|
+
readonly origin: Origin;
|
|
97
|
+
}): ReactElement | null;
|
|
98
|
+
/**
|
|
99
|
+
* Where this change's renders stopped agreeing with each other.
|
|
100
|
+
*
|
|
101
|
+
* A change is approved once and lands many times, and the count above reads as
|
|
102
|
+
* one thing having happened seven times. On a component with two shapes it is
|
|
103
|
+
* two things: two renders moved by 6,418 pixels and five by about 3,500, and a
|
|
104
|
+
* reviewer pressing approve is signing off on both under one name.
|
|
105
|
+
*
|
|
106
|
+
* So the groups are drawn, largest first, with what each one is worth in pixels
|
|
107
|
+
* and the name its renders share when that name is theirs alone. The digest goes
|
|
108
|
+
* beside each group rather than in the prose, because `variance accept --shape`
|
|
109
|
+
* takes exactly that set in the next run and a reviewer who has decided one of
|
|
110
|
+
* the two groups is holding the argument for it.
|
|
111
|
+
*/
|
|
112
|
+
export declare function Shapes({ origin }: {
|
|
113
|
+
readonly origin: Origin;
|
|
114
|
+
}): ReactElement | null;
|
|
115
|
+
/**
|
|
116
|
+
* How often this component moves, asked on mount: there is one change on this page.
|
|
117
|
+
*
|
|
118
|
+
* Named for the question rather than the call, and not `Record`, which is a
|
|
119
|
+
* global type every other file in this package is entitled to use.
|
|
120
|
+
*/
|
|
121
|
+
export declare function Recurrence({ client, component, }: {
|
|
122
|
+
readonly client: ReviewClient;
|
|
123
|
+
readonly component: string;
|
|
124
|
+
}): ReactElement | null;
|
|
125
|
+
/**
|
|
126
|
+
* Collateral, and nothing at all when the build did not count it.
|
|
127
|
+
*
|
|
128
|
+
* One number for the whole build, repeated onto every cause row by the store —
|
|
129
|
+
* which is exactly why it is printed once, here, and never attributed to a
|
|
130
|
+
* change. Deciding which edit pushed which box around is the attribution the
|
|
131
|
+
* semantic tier declined to make. The guard is the rule: a build with no causes
|
|
132
|
+
* has not counted zero collateral pixels, it has counted none, and `0 collateral
|
|
133
|
+
* pixels` reads as a measurement.
|
|
134
|
+
*/
|
|
135
|
+
export declare function Collateral({ build }: {
|
|
136
|
+
readonly build: BuildDetail;
|
|
137
|
+
}): ReactElement | null;
|
|
138
|
+
/**
|
|
139
|
+
* What a band claim is made of, said once on the page that makes them.
|
|
140
|
+
*
|
|
141
|
+
* Asked directly, about `what it announces`: *how do you know?* The page had no
|
|
142
|
+
* answer on it. It knows because two digests disagree, and the digest is over the
|
|
143
|
+
* role, name and state of every node inside the boundary — which is enough to
|
|
144
|
+
* assert the band and not enough to say what the name became, because the
|
|
145
|
+
* baseline sidecar stores the hash and not the value.
|
|
146
|
+
*
|
|
147
|
+
* At the foot rather than beside every band, because it is the same sentence for
|
|
148
|
+
* all of them and a reviewer needs it once. Not behind a disclosure: the whole
|
|
149
|
+
* complaint that produced it was a page processing its results and hiding what
|
|
150
|
+
* they were made from.
|
|
151
|
+
*/
|
|
152
|
+
export declare function Evidence(): ReactElement;
|
|
153
|
+
//# sourceMappingURL=change-story.d.ts.map
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* What the record says about one change, before anybody decides it.
|
|
4
|
+
*
|
|
5
|
+
* Seven readings, and not one of them is an act. [`change.tsx`](./change.tsx)
|
|
6
|
+
* holds the page and the two buttons; this holds every sentence printed above
|
|
7
|
+
* them, which is the half that has to be argued rather than wired — what counts
|
|
8
|
+
* as *reached*, what a silence is allowed to imply, and which of two absences a
|
|
9
|
+
* reviewer is looking at. Splitting them keeps the argument readable and stops
|
|
10
|
+
* the page that carries the decision from being mostly prose about it.
|
|
11
|
+
*
|
|
12
|
+
* The rule they share: absent is not empty. A run that read no diff, a component
|
|
13
|
+
* the source index does not declare, a render nobody wrote a reach row for and a
|
|
14
|
+
* build that counted no collateral each get their own sentence or no sentence at
|
|
15
|
+
* all, because the alternative is spending this product’s loudest line on its
|
|
16
|
+
* most ordinary fact and training the reader out of it.
|
|
17
|
+
*/
|
|
18
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
19
|
+
import { shapesOf } from './grouping.js';
|
|
20
|
+
import { partedBy } from './parted.js';
|
|
21
|
+
import { ChurnLine } from './history.js';
|
|
22
|
+
import { ORDER, READS, senseOf } from './sense.js';
|
|
23
|
+
import { count, number } from './text.js';
|
|
24
|
+
/**
|
|
25
|
+
* Three counts, because the reviewer asks three questions and they differ.
|
|
26
|
+
*
|
|
27
|
+
* *2 distinct differences, in 7 renders* is two of them run together, and the
|
|
28
|
+
* third — how many of those a press of the button actually settles — was in the
|
|
29
|
+
* button's own label, at the other end of the header. The three come apart on
|
|
30
|
+
* every change of any size: a component moves in twelve renders, five of which
|
|
31
|
+
* the docket filed under somebody else; the twelve carry two distinct shapes, so
|
|
32
|
+
* there are two pictures to look at rather than twelve; and seven are open here,
|
|
33
|
+
* because the rest are decided or belong to the other change.
|
|
34
|
+
*
|
|
35
|
+
* Kept as three cells with the reviewer's own words on them rather than folded
|
|
36
|
+
* into a sentence. A sentence has to pick an order and an emphasis, and these
|
|
37
|
+
* are three answers of equal standing.
|
|
38
|
+
*
|
|
39
|
+
* The pixel total rides along, muted. It is the least useful figure available —
|
|
40
|
+
* one number for every size of change — and it is the one somebody eventually
|
|
41
|
+
* asks for.
|
|
42
|
+
*/
|
|
43
|
+
export function Tally({ origin, elsewhere, open, }) {
|
|
44
|
+
const shapes = shapesOf(origin.appearances).size;
|
|
45
|
+
const know = origin.appearances.length + elsewhere;
|
|
46
|
+
return (_jsxs("p", { className: "va-tally", children: [_jsxs("span", { title: "Every render this component moved in, wherever the docket filed it.", children: [_jsx("strong", { children: count(know, 'render') }), " to know about"] }), _jsx("span", { title: "Distinct shapes among the renders on this page \u2014 how many pictures there are to look at, rather than how many renders carry them.", children: shapes === 0 ? (_jsxs(_Fragment, { children: [_jsx("strong", { children: "no shape" }), " was recorded, so nothing says whether these moved the same way"] })) : (_jsxs(_Fragment, { children: [_jsx("strong", { children: count(shapes, 'difference') }), " to review"] })) }), _jsx("span", { title: "Renders on this page that are open and approvable \u2014 what pressing approve settles.", children: open === 0 ? (_jsxs(_Fragment, { children: [_jsx("strong", { children: "nothing" }), " left to accept"] })) : (_jsxs(_Fragment, { children: [_jsx("strong", { children: count(open, 'render') }), " to accept"] })) }), _jsxs("span", { className: "va-note", children: [number(origin.pixels), " px"] })] }));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Where this component is declared, and what it means when nothing says.
|
|
50
|
+
*
|
|
51
|
+
* A blank is the one answer this cell must not give. The reviewer's next move on
|
|
52
|
+
* a change they do not recognise is to open the file, and *nothing here* reads as
|
|
53
|
+
* a defect in the tool — when the ordinary cause is that the name belongs to a
|
|
54
|
+
* dependency, which the source index scans no part of and never claimed to. So
|
|
55
|
+
* the two silences are separated: a run that resolved files for other components
|
|
56
|
+
* has said something about this one, and a run that resolved none has not.
|
|
57
|
+
*/
|
|
58
|
+
export function Declared({ file, sourced, }) {
|
|
59
|
+
if (file !== undefined)
|
|
60
|
+
return _jsx("code", { className: "va-file", children: file });
|
|
61
|
+
return (_jsx("span", { className: "va-note", title: sourced
|
|
62
|
+
? 'This run resolved files for other components in this build, so this is a name its source index does not declare — a component out of a dependency, or one produced at build time.'
|
|
63
|
+
: 'This run resolved no source files at all, so nothing here says where any of these components are declared.', children: sourced ? 'not in the scanned source' : 'no source index' }));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether the commit arrives here, said in five states rather than two.
|
|
67
|
+
*
|
|
68
|
+
* A run with no diff read cannot say, and saying nothing is the only honest
|
|
69
|
+
* version of that. Silence would be indistinguishable from *reached*, which is
|
|
70
|
+
* the assumption a reviewer makes by default and the one that costs them.
|
|
71
|
+
*
|
|
72
|
+
* The last of the five is now conditional: it is a reconstruction, and it stands
|
|
73
|
+
* down where the run recorded a conclusion of its own. See the branch below.
|
|
74
|
+
*
|
|
75
|
+
* The rest is the split this card got wrong twice. `reached` is computed against
|
|
76
|
+
* the components the diff can arrive at, so a component the graph carries nowhere
|
|
77
|
+
* — anything out of `node_modules`, which is most of the host nodes on a real
|
|
78
|
+
* page — can never be in that set whatever the commit did. And a render the reach
|
|
79
|
+
* record has no row for is not a render the commit fails to reach; it is a render
|
|
80
|
+
* nobody wrote down. Printing *nothing reaches it and it moved anyway* over
|
|
81
|
+
* either of those spends the loudest sentence on the page on the most ordinary
|
|
82
|
+
* fact about it, and by the time a real orphan appears the sentence has been
|
|
83
|
+
* trained out of the reader.
|
|
84
|
+
*/
|
|
85
|
+
export function Arrival({ origin }) {
|
|
86
|
+
if (origin.reached === undefined)
|
|
87
|
+
return null;
|
|
88
|
+
if (origin.reached) {
|
|
89
|
+
const trail = origin.trail;
|
|
90
|
+
return (_jsx("p", { className: "va-reaches va-reached", children: trail === undefined ? ('This commit reaches it.') : (_jsxs(_Fragment, { children: ["This commit reaches it:", ' ', trail.map((step, index) => (_jsxs("span", { children: [index === 0 ? null : _jsx("span", { className: "va-arrow", children: "\u2192" }), index === trail.length - 1 ? _jsx("strong", { children: step }) : _jsx("code", { children: step })] }, step)))] })) }));
|
|
91
|
+
}
|
|
92
|
+
const stranded = origin.stranded ?? [];
|
|
93
|
+
const unlisted = origin.unlisted ?? [];
|
|
94
|
+
const through = origin.through ?? [];
|
|
95
|
+
if (stranded.length > 0) {
|
|
96
|
+
return (_jsxs("p", { className: "va-reaches va-alarm", children: ["This commit reaches nothing at all in ", count(stranded.length, 'render'), " it moved in \u2014", ' ', stranded.slice(0, 3).join(', '), stranded.length > 3 ? `, and ${count(stranded.length - 3, 'other')}` : '', ". Something changed there that the diff cannot account for."] }));
|
|
97
|
+
}
|
|
98
|
+
// The run's own conclusion supersedes the rest of this, and `Because` has
|
|
99
|
+
// already printed it directly above. What follows is a reconstruction from the
|
|
100
|
+
// reach record alone — and on a component the import walk merely climbed past,
|
|
101
|
+
// it reads *this comes from a dependency* about a file named at the top of this
|
|
102
|
+
// same page. The one thing it says that the conclusion does not is the gap in
|
|
103
|
+
// the record, so that is what survives.
|
|
104
|
+
if (origin.cause !== undefined) {
|
|
105
|
+
return unlisted.length === 0 ? null : (_jsx("p", { className: "va-reaches va-unnamed", children: _jsx(Unlisted, { renders: unlisted.length }) }));
|
|
106
|
+
}
|
|
107
|
+
return (_jsxs("p", { className: "va-reaches va-unnamed", children: ["The commit reaches nothing called ", _jsx("strong", { children: origin.component }), " \u2014 the file graph carries no such name, which is what a component out of a dependency looks like from here.", ' ', through.length === 0
|
|
108
|
+
? 'It does reach every render this moved in, without naming a component in any of them.'
|
|
109
|
+
: `It does reach every render this moved in, through ${through.slice(0, 4).join(', ')}${through.length > 4 ? ` and ${count(through.length - 4, 'other')}` : ''} — so either one of those drew this node, or something reaches it that the graph does not model.`, unlisted.length === 0 ? null : (_jsxs(_Fragment, { children: [' ', _jsx(Unlisted, { renders: unlisted.length })] }))] }));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The renders the reach record has no row for.
|
|
113
|
+
*
|
|
114
|
+
* Not *the commit does not reach these*. Nobody wrote them down, and a page that
|
|
115
|
+
* folded the two together would report a gap in its own record as a finding about
|
|
116
|
+
* the commit.
|
|
117
|
+
*/
|
|
118
|
+
function Unlisted({ renders }) {
|
|
119
|
+
return (_jsxs(_Fragment, { children: [count(renders, 'render'), " it moved in ", renders === 1 ? 'has' : 'have', " no reach recorded at all, so nothing the commit reaches was ever weighed against", ' ', renders === 1 ? 'it' : 'them', "."] }));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* What the run before this one had to say about these renders.
|
|
123
|
+
*
|
|
124
|
+
* The sentence a reviewer skips work on, and the reason it is here rather than in
|
|
125
|
+
* the crossing panel: *build 5 already showed you this, and nobody decided it* is
|
|
126
|
+
* worth reading beside the change it is about and worth nothing at the bottom of
|
|
127
|
+
* a page about twenty other subjects. The undecided count is the part that is
|
|
128
|
+
* never printed anywhere else — it is the docket being delivered twice.
|
|
129
|
+
*/
|
|
130
|
+
export function SinceLast({ crossing, origin, }) {
|
|
131
|
+
if (crossing.state !== 'ready')
|
|
132
|
+
return null;
|
|
133
|
+
const rows = origin.appearances
|
|
134
|
+
.map(({ subject }) => crossing.of(subject.subject))
|
|
135
|
+
.filter((row) => row !== undefined);
|
|
136
|
+
if (rows.length === 0)
|
|
137
|
+
return null;
|
|
138
|
+
const against = `build ${crossing.earlier.build}`;
|
|
139
|
+
const seen = rows.filter((row) => row.shift === 'again');
|
|
140
|
+
const undecided = seen.filter((row) => row.earlier?.decision == null).length;
|
|
141
|
+
const moved = rows.filter((row) => row.shift === 'differently').length;
|
|
142
|
+
const first = rows.filter((row) => row.shift === 'first').length;
|
|
143
|
+
const fresh = rows.filter((row) => row.shift === 'new').length;
|
|
144
|
+
if (seen.length === 0 && moved === 0 && first === 0 && fresh === 0)
|
|
145
|
+
return null;
|
|
146
|
+
return (_jsxs("p", { className: seen.length > 0 ? 'va-since va-known' : 'va-since', children: [seen.length === 0 ? null : (_jsxs(_Fragment, { children: [seen.length === rows.length ? 'Every one of these' : count(seen.length, 'of these'), ' ', "carried the same difference in ", against, undecided === 0
|
|
147
|
+
? ', and every one was decided there.'
|
|
148
|
+
: undecided === seen.length
|
|
149
|
+
? ', and none of them was decided there.'
|
|
150
|
+
: `, and ${number(undecided)} of them went undecided there.`, ' '] })), moved === 0 ? null : (_jsxs(_Fragment, { children: [count(moved, 'render'), " differed in ", against, " too, but not in the same way: the difference itself changed between the two builds.", ' '] })), first === 0 ? null : (_jsxs(_Fragment, { children: [count(first, 'render'), " moved for the first time; ", against, " compared", ' ', first === 1 ? 'it' : 'them', " and found nothing.", ' '] })), fresh === 0 ? null : _jsxs(_Fragment, { children: [count(fresh, 'render'), " did not exist in ", against, "."] })] }));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A shape at reading length, with the whole of it a hover away.
|
|
154
|
+
*
|
|
155
|
+
* The digest is thirty-two hexadecimal characters and a reviewer needs none of
|
|
156
|
+
* them: it is an identity, and an identity is only ever typed by copying it. A
|
|
157
|
+
* prefix says *this one and not that one* on a page that never shows two, which
|
|
158
|
+
* is the whole of the job the full string was doing on the surface.
|
|
159
|
+
*/
|
|
160
|
+
function brief(shape) {
|
|
161
|
+
return shape.length <= 14 ? shape : `${shape.slice(0, 14)}…`;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Where this change's renders stopped agreeing with each other.
|
|
165
|
+
*
|
|
166
|
+
* A change is approved once and lands many times, and the count above reads as
|
|
167
|
+
* one thing having happened seven times. On a component with two shapes it is
|
|
168
|
+
* two things: two renders moved by 6,418 pixels and five by about 3,500, and a
|
|
169
|
+
* reviewer pressing approve is signing off on both under one name.
|
|
170
|
+
*
|
|
171
|
+
* So the groups are drawn, largest first, with what each one is worth in pixels
|
|
172
|
+
* and the name its renders share when that name is theirs alone. The digest goes
|
|
173
|
+
* beside each group rather than in the prose, because `variance accept --shape`
|
|
174
|
+
* takes exactly that set in the next run and a reviewer who has decided one of
|
|
175
|
+
* the two groups is holding the argument for it.
|
|
176
|
+
*/
|
|
177
|
+
export function Shapes({ origin }) {
|
|
178
|
+
const { shapes, unshaped } = partedBy(origin);
|
|
179
|
+
if (shapes.length === 0)
|
|
180
|
+
return null;
|
|
181
|
+
const only = shapes.length === 1 && unshaped.length === 0 ? shapes[0] : undefined;
|
|
182
|
+
if (only !== undefined) {
|
|
183
|
+
return (_jsxs("p", { className: "va-note", children: ["The same difference in every one of the ", count(origin.appearances.length, 'render'), ".", ' ', _jsxs("code", { title: only.shape, children: ["variance accept --shape ", brief(only.shape)] }), " takes exactly this set."] }));
|
|
184
|
+
}
|
|
185
|
+
return (_jsxs("section", { className: "va-parted", children: [_jsxs("h2", { children: [count(shapes.length, 'shape'), " over ", count(origin.appearances.length, 'render')] }), _jsx("ul", { children: shapes.map((each) => (_jsxs("li", { children: [_jsxs("p", { className: "va-parted-head", children: [_jsxs("span", { className: "va-num", children: [span(each.least, each.most), " px"] }), each.only === undefined ? null : _jsx("span", { className: "va-parted-only", children: each.only }), _jsxs("code", { className: "va-parted-take", title: each.shape, children: ["--shape ", brief(each.shape)] })] }), _jsx("p", { className: "va-parted-in va-note", children: each.renders.join(', ') })] }, each.shape))) }), unshaped.length === 0 ? null : (_jsxs("p", { className: "va-note", children: [count(unshaped.length, 'render'), " recorded no shape, so they are not known to match anything here: ", unshaped.join(', '), "."] }))] }));
|
|
186
|
+
}
|
|
187
|
+
/** One figure when the group is flat, two when the renders differ in size. */
|
|
188
|
+
function span(least, most) {
|
|
189
|
+
return least === most ? number(least) : `${number(least)}–${number(most)}`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* How often this component moves, asked on mount: there is one change on this page.
|
|
193
|
+
*
|
|
194
|
+
* Named for the question rather than the call, and not `Record`, which is a
|
|
195
|
+
* global type every other file in this package is entitled to use.
|
|
196
|
+
*/
|
|
197
|
+
export function Recurrence({ client, component, }) {
|
|
198
|
+
const [churn, setChurn] = useState(null);
|
|
199
|
+
const load = useCallback(async () => {
|
|
200
|
+
try {
|
|
201
|
+
setChurn(await client.churn(component));
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// A missing record is a silence this page can afford. Everything above it
|
|
205
|
+
// is a reading of this build, and none of it becomes less true because the
|
|
206
|
+
// history service did not answer.
|
|
207
|
+
setChurn(null);
|
|
208
|
+
}
|
|
209
|
+
}, [client, component]);
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
void load();
|
|
212
|
+
}, [load]);
|
|
213
|
+
// `0 of 0 runs` on every change is the shape of an answer with none of the
|
|
214
|
+
// substance, and this is a reading of *this* build — history is an addition
|
|
215
|
+
// where there is history, not a line that has to be filled.
|
|
216
|
+
if (churn === null || churn.runs === 0)
|
|
217
|
+
return null;
|
|
218
|
+
return _jsx(ChurnLine, { component: component, churn: churn });
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Collateral, and nothing at all when the build did not count it.
|
|
222
|
+
*
|
|
223
|
+
* One number for the whole build, repeated onto every cause row by the store —
|
|
224
|
+
* which is exactly why it is printed once, here, and never attributed to a
|
|
225
|
+
* change. Deciding which edit pushed which box around is the attribution the
|
|
226
|
+
* semantic tier declined to make. The guard is the rule: a build with no causes
|
|
227
|
+
* has not counted zero collateral pixels, it has counted none, and `0 collateral
|
|
228
|
+
* pixels` reads as a measurement.
|
|
229
|
+
*/
|
|
230
|
+
export function Collateral({ build }) {
|
|
231
|
+
const pixels = build.causes[0]?.collateralPixels;
|
|
232
|
+
if (pixels === undefined || pixels === 0)
|
|
233
|
+
return null;
|
|
234
|
+
return (_jsxs("p", { className: "va-note va-collateral-note", children: [count(pixels, 'collateral pixel'), " moved in this build alongside the changes \u2014 regions that shifted because something else did. Counted for the build, never split between the changes."] }));
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* What a band claim is made of, said once on the page that makes them.
|
|
238
|
+
*
|
|
239
|
+
* Asked directly, about `what it announces`: *how do you know?* The page had no
|
|
240
|
+
* answer on it. It knows because two digests disagree, and the digest is over the
|
|
241
|
+
* role, name and state of every node inside the boundary — which is enough to
|
|
242
|
+
* assert the band and not enough to say what the name became, because the
|
|
243
|
+
* baseline sidecar stores the hash and not the value.
|
|
244
|
+
*
|
|
245
|
+
* At the foot rather than beside every band, because it is the same sentence for
|
|
246
|
+
* all of them and a reviewer needs it once. Not behind a disclosure: the whole
|
|
247
|
+
* complaint that produced it was a page processing its results and hiding what
|
|
248
|
+
* they were made from.
|
|
249
|
+
*/
|
|
250
|
+
export function Evidence() {
|
|
251
|
+
return (_jsxs("p", { className: "va-evidence", children: ["Each band is a digest comparison \u2014", ' ', ORDER.filter((band) => band !== 'texture').map((band, index) => (_jsxs("span", { children: [index === 0 ? '' : '; ', _jsx("em", { children: senseOf(band) }), " over ", READS[band]] }, band))), ". The baseline keeps the digests and not the values, so a band names which of them differ and not what they became."] }));
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=change-story.js.map
|