@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.
Files changed (271) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/LICENSE +21 -0
  3. package/README.md +506 -0
  4. package/dist/__fixtures__/review.d.ts +47 -0
  5. package/dist/__fixtures__/review.js +145 -0
  6. package/dist/__fixtures__/review.js.map +1 -0
  7. package/dist/bindings.d.ts +134 -0
  8. package/dist/bindings.js +88 -0
  9. package/dist/bindings.js.map +1 -0
  10. package/dist/changelog.d.ts +132 -0
  11. package/dist/changelog.js +129 -0
  12. package/dist/changelog.js.map +1 -0
  13. package/dist/history-queries.d.ts +82 -0
  14. package/dist/history-queries.js +200 -0
  15. package/dist/history-queries.js.map +1 -0
  16. package/dist/history-rows.d.ts +42 -0
  17. package/dist/history-rows.js +148 -0
  18. package/dist/history-rows.js.map +1 -0
  19. package/dist/history.d.ts +40 -0
  20. package/dist/history.js +186 -0
  21. package/dist/history.js.map +1 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.js +12 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/migration-steps.d.ts +32 -0
  26. package/dist/migration-steps.js +303 -0
  27. package/dist/migration-steps.js.map +1 -0
  28. package/dist/migrations.d.ts +58 -0
  29. package/dist/migrations.js +283 -0
  30. package/dist/migrations.js.map +1 -0
  31. package/dist/next.d.ts +94 -0
  32. package/dist/next.js +44 -0
  33. package/dist/next.js.map +1 -0
  34. package/dist/node/bin.d.ts +120 -0
  35. package/dist/node/bin.js +337 -0
  36. package/dist/node/bin.js.map +1 -0
  37. package/dist/node/bucket.d.ts +58 -0
  38. package/dist/node/bucket.js +142 -0
  39. package/dist/node/bucket.js.map +1 -0
  40. package/dist/node/database.d.ts +33 -0
  41. package/dist/node/database.js +157 -0
  42. package/dist/node/database.js.map +1 -0
  43. package/dist/node/index.d.ts +43 -0
  44. package/dist/node/index.js +43 -0
  45. package/dist/node/index.js.map +1 -0
  46. package/dist/node/serve.d.ts +81 -0
  47. package/dist/node/serve.js +193 -0
  48. package/dist/node/serve.js.map +1 -0
  49. package/dist/node/ui-assets.d.ts +47 -0
  50. package/dist/node/ui-assets.js +60 -0
  51. package/dist/node/ui-assets.js.map +1 -0
  52. package/dist/review-ingest.d.ts +24 -0
  53. package/dist/review-ingest.js +184 -0
  54. package/dist/review-ingest.js.map +1 -0
  55. package/dist/review-read.d.ts +87 -0
  56. package/dist/review-read.js +361 -0
  57. package/dist/review-read.js.map +1 -0
  58. package/dist/review-rows.d.ts +30 -0
  59. package/dist/review-rows.js +67 -0
  60. package/dist/review-rows.js.map +1 -0
  61. package/dist/review-types.d.ts +414 -0
  62. package/dist/review-types.js +2 -0
  63. package/dist/review-types.js.map +1 -0
  64. package/dist/review-write.d.ts +35 -0
  65. package/dist/review-write.js +86 -0
  66. package/dist/review-write.js.map +1 -0
  67. package/dist/review.d.ts +53 -0
  68. package/dist/review.fixtures.d.ts +47 -0
  69. package/dist/review.fixtures.js +145 -0
  70. package/dist/review.fixtures.js.map +1 -0
  71. package/dist/review.js +245 -0
  72. package/dist/review.js.map +1 -0
  73. package/dist/schema.d.ts +36 -0
  74. package/dist/schema.js +39 -0
  75. package/dist/schema.js.map +1 -0
  76. package/dist/store.d.ts +58 -0
  77. package/dist/store.js +278 -0
  78. package/dist/store.js.map +1 -0
  79. package/dist/testing.d.ts +38 -0
  80. package/dist/testing.js +92 -0
  81. package/dist/testing.js.map +1 -0
  82. package/dist/ui/__probe.d.ts +3 -0
  83. package/dist/ui/__probe.js +5 -0
  84. package/dist/ui/__probe.js.map +1 -0
  85. package/dist/ui/attribution.d.ts +46 -0
  86. package/dist/ui/attribution.js +79 -0
  87. package/dist/ui/attribution.js.map +1 -0
  88. package/dist/ui/because.d.ts +51 -0
  89. package/dist/ui/because.js +52 -0
  90. package/dist/ui/because.js.map +1 -0
  91. package/dist/ui/browser-entry.d.ts +2 -0
  92. package/dist/ui/browser-entry.js +34 -0
  93. package/dist/ui/browser-entry.js.map +1 -0
  94. package/dist/ui/builds.d.ts +43 -0
  95. package/dist/ui/builds.js +79 -0
  96. package/dist/ui/builds.js.map +1 -0
  97. package/dist/ui/carried.d.ts +73 -0
  98. package/dist/ui/carried.js +63 -0
  99. package/dist/ui/carried.js.map +1 -0
  100. package/dist/ui/cause.d.ts +73 -0
  101. package/dist/ui/cause.js +133 -0
  102. package/dist/ui/cause.js.map +1 -0
  103. package/dist/ui/change-story.d.ts +153 -0
  104. package/dist/ui/change-story.js +253 -0
  105. package/dist/ui/change-story.js.map +1 -0
  106. package/dist/ui/change.d.ts +51 -0
  107. package/dist/ui/change.js +125 -0
  108. package/dist/ui/change.js.map +1 -0
  109. package/dist/ui/client.d.ts +74 -0
  110. package/dist/ui/client.js +88 -0
  111. package/dist/ui/client.js.map +1 -0
  112. package/dist/ui/consumers.d.ts +80 -0
  113. package/dist/ui/consumers.js +125 -0
  114. package/dist/ui/consumers.js.map +1 -0
  115. package/dist/ui/control.d.ts +63 -0
  116. package/dist/ui/control.js +39 -0
  117. package/dist/ui/control.js.map +1 -0
  118. package/dist/ui/crossing.d.ts +55 -0
  119. package/dist/ui/crossing.js +74 -0
  120. package/dist/ui/crossing.js.map +1 -0
  121. package/dist/ui/declarations.d.ts +36 -0
  122. package/dist/ui/declarations.js +73 -0
  123. package/dist/ui/declarations.js.map +1 -0
  124. package/dist/ui/distance.d.ts +154 -0
  125. package/dist/ui/distance.js +180 -0
  126. package/dist/ui/distance.js.map +1 -0
  127. package/dist/ui/divergence.d.ts +40 -0
  128. package/dist/ui/divergence.js +176 -0
  129. package/dist/ui/divergence.js.map +1 -0
  130. package/dist/ui/docket.d.ts +39 -0
  131. package/dist/ui/docket.js +172 -0
  132. package/dist/ui/docket.js.map +1 -0
  133. package/dist/ui/findings.d.ts +42 -0
  134. package/dist/ui/findings.js +110 -0
  135. package/dist/ui/findings.js.map +1 -0
  136. package/dist/ui/foreseen.d.ts +55 -0
  137. package/dist/ui/foreseen.js +91 -0
  138. package/dist/ui/foreseen.js.map +1 -0
  139. package/dist/ui/glance.d.ts +101 -0
  140. package/dist/ui/glance.js +101 -0
  141. package/dist/ui/glance.js.map +1 -0
  142. package/dist/ui/grouping.d.ts +158 -0
  143. package/dist/ui/grouping.js +187 -0
  144. package/dist/ui/grouping.js.map +1 -0
  145. package/dist/ui/handed.d.ts +60 -0
  146. package/dist/ui/handed.js +122 -0
  147. package/dist/ui/handed.js.map +1 -0
  148. package/dist/ui/history.d.ts +88 -0
  149. package/dist/ui/history.js +170 -0
  150. package/dist/ui/history.js.map +1 -0
  151. package/dist/ui/holding.d.ts +84 -0
  152. package/dist/ui/holding.js +90 -0
  153. package/dist/ui/holding.js.map +1 -0
  154. package/dist/ui/impact.d.ts +31 -0
  155. package/dist/ui/impact.js +132 -0
  156. package/dist/ui/impact.js.map +1 -0
  157. package/dist/ui/index.d.ts +21 -0
  158. package/dist/ui/index.js +21 -0
  159. package/dist/ui/index.js.map +1 -0
  160. package/dist/ui/lead.d.ts +35 -0
  161. package/dist/ui/lead.js +34 -0
  162. package/dist/ui/lead.js.map +1 -0
  163. package/dist/ui/look.d.ts +109 -0
  164. package/dist/ui/look.js +248 -0
  165. package/dist/ui/look.js.map +1 -0
  166. package/dist/ui/mark.d.ts +11 -0
  167. package/dist/ui/mark.js +13 -0
  168. package/dist/ui/mark.js.map +1 -0
  169. package/dist/ui/moved.d.ts +121 -0
  170. package/dist/ui/moved.js +143 -0
  171. package/dist/ui/moved.js.map +1 -0
  172. package/dist/ui/navigation.d.ts +26 -0
  173. package/dist/ui/navigation.js +53 -0
  174. package/dist/ui/navigation.js.map +1 -0
  175. package/dist/ui/order.d.ts +113 -0
  176. package/dist/ui/order.js +226 -0
  177. package/dist/ui/order.js.map +1 -0
  178. package/dist/ui/origins.d.ts +42 -0
  179. package/dist/ui/origins.js +149 -0
  180. package/dist/ui/origins.js.map +1 -0
  181. package/dist/ui/outcome.d.ts +93 -0
  182. package/dist/ui/outcome.js +162 -0
  183. package/dist/ui/outcome.js.map +1 -0
  184. package/dist/ui/parted.d.ts +68 -0
  185. package/dist/ui/parted.js +101 -0
  186. package/dist/ui/parted.js.map +1 -0
  187. package/dist/ui/rail.d.ts +31 -0
  188. package/dist/ui/rail.js +21 -0
  189. package/dist/ui/rail.js.map +1 -0
  190. package/dist/ui/reach.d.ts +66 -0
  191. package/dist/ui/reach.js +185 -0
  192. package/dist/ui/reach.js.map +1 -0
  193. package/dist/ui/regions.d.ts +82 -0
  194. package/dist/ui/regions.js +67 -0
  195. package/dist/ui/regions.js.map +1 -0
  196. package/dist/ui/review.bundle.js +69 -0
  197. package/dist/ui/review.d.ts +97 -0
  198. package/dist/ui/review.js +39 -0
  199. package/dist/ui/review.js.map +1 -0
  200. package/dist/ui/root.d.ts +72 -0
  201. package/dist/ui/root.js +102 -0
  202. package/dist/ui/root.js.map +1 -0
  203. package/dist/ui/route.d.ts +95 -0
  204. package/dist/ui/route.js +114 -0
  205. package/dist/ui/route.js.map +1 -0
  206. package/dist/ui/run.d.ts +26 -0
  207. package/dist/ui/run.js +12 -0
  208. package/dist/ui/run.js.map +1 -0
  209. package/dist/ui/sense.d.ts +187 -0
  210. package/dist/ui/sense.js +222 -0
  211. package/dist/ui/sense.js.map +1 -0
  212. package/dist/ui/settled.d.ts +43 -0
  213. package/dist/ui/settled.js +59 -0
  214. package/dist/ui/settled.js.map +1 -0
  215. package/dist/ui/shell.d.ts +76 -0
  216. package/dist/ui/shell.js +65 -0
  217. package/dist/ui/shell.js.map +1 -0
  218. package/dist/ui/shift.d.ts +52 -0
  219. package/dist/ui/shift.js +98 -0
  220. package/dist/ui/shift.js.map +1 -0
  221. package/dist/ui/styles-docket.d.ts +18 -0
  222. package/dist/ui/styles-docket.js +281 -0
  223. package/dist/ui/styles-docket.js.map +1 -0
  224. package/dist/ui/styles-stage.d.ts +18 -0
  225. package/dist/ui/styles-stage.js +92 -0
  226. package/dist/ui/styles-stage.js.map +1 -0
  227. package/dist/ui/styles.d.ts +12 -0
  228. package/dist/ui/styles.js +482 -0
  229. package/dist/ui/styles.js.map +1 -0
  230. package/dist/ui/subject.d.ts +36 -0
  231. package/dist/ui/subject.js +138 -0
  232. package/dist/ui/subject.js.map +1 -0
  233. package/dist/ui/text.d.ts +129 -0
  234. package/dist/ui/text.js +164 -0
  235. package/dist/ui/text.js.map +1 -0
  236. package/dist/ui/variations.d.ts +37 -0
  237. package/dist/ui/variations.js +60 -0
  238. package/dist/ui/variations.js.map +1 -0
  239. package/dist/ui/viewer.d.ts +67 -0
  240. package/dist/ui/viewer.js +246 -0
  241. package/dist/ui/viewer.js.map +1 -0
  242. package/dist/worker-auth.d.ts +48 -0
  243. package/dist/worker-auth.js +74 -0
  244. package/dist/worker-auth.js.map +1 -0
  245. package/dist/worker-entry.d.ts +58 -0
  246. package/dist/worker-entry.js +36 -0
  247. package/dist/worker-entry.js.map +1 -0
  248. package/dist/worker-http.d.ts +39 -0
  249. package/dist/worker-http.js +111 -0
  250. package/dist/worker-http.js.map +1 -0
  251. package/dist/worker-input.d.ts +81 -0
  252. package/dist/worker-input.js +296 -0
  253. package/dist/worker-input.js.map +1 -0
  254. package/dist/worker.d.ts +91 -0
  255. package/dist/worker.js +287 -0
  256. package/dist/worker.js.map +1 -0
  257. package/mark.svg +30 -0
  258. package/migrations/0001_initial.sql +193 -0
  259. package/migrations/0002_changelog.sql +30 -0
  260. package/migrations/0003_accessibility.sql +8 -0
  261. package/migrations/0004_variations.sql +19 -0
  262. package/migrations/0005_reach.sql +26 -0
  263. package/migrations/0006_baseline-size.sql +7 -0
  264. package/migrations/0007_declarations.sql +7 -0
  265. package/migrations/0008_green-because.sql +7 -0
  266. package/migrations/0009_moved-bands.sql +6 -0
  267. package/migrations/0010_composition.sql +15 -0
  268. package/migrations/0011_movements.sql +21 -0
  269. package/migrations/0012_control-size.sql +6 -0
  270. package/package.json +110 -0
  271. package/wrangler.jsonc +45 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A build: the docket on the left, one thing open beside it.
3
+ *
4
+ * Two panes, and neither of them is the document. The page itself does not
5
+ * scroll — the rail scrolls through the changes, the stage scrolls through the
6
+ * one that is open, and the address says which. That is the difference between a
7
+ * service and a report: a report is a column of everything, read top to bottom
8
+ * once; a service is a place a reviewer navigates, comes back to, sends a link
9
+ * into, and leaves a browser tab on.
10
+ *
11
+ * The four addresses this page answers are four stages behind one rail:
12
+ *
13
+ * - **`/builds/:id`** — the rail with nothing open, and a short reading of the
14
+ * run beside it. Not the encyclopedia: that is `/run`, and it is a link.
15
+ * - **`/builds/:id/changes/:component`** — one change, whole, with its own
16
+ * picture and its own two buttons.
17
+ * - **`/builds/:id/subjects/:subject`** — one render, whole, with the rail
18
+ * switched to subjects.
19
+ * - **`/builds/:id/run`** — everything the run read, full width.
20
+ *
21
+ * The crossing against the previous build is fetched **here**, once, and handed
22
+ * to all four. It is what lets a change card say *build 5 showed you this and
23
+ * nobody decided it* without a second request per card, and it is the reason the
24
+ * run page and a change can never disagree about what the last run said.
25
+ */
26
+ import { type ReactElement } from 'react';
27
+ import type { ReviewClient } from './client.js';
28
+ import type { Route } from './route.js';
29
+ /** The four addresses that live inside one build. */
30
+ export type BuildRoute = Extract<Route, {
31
+ page: 'build' | 'change' | 'subject' | 'run';
32
+ }>;
33
+ export declare function BuildPage({ client, reviewer, route, go, }: {
34
+ readonly client: ReviewClient;
35
+ readonly reviewer: string;
36
+ readonly route: BuildRoute;
37
+ readonly go: (route: Route) => void;
38
+ }): ReactElement;
39
+ //# sourceMappingURL=docket.d.ts.map
@@ -0,0 +1,172 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A build: the docket on the left, one thing open beside it.
4
+ *
5
+ * Two panes, and neither of them is the document. The page itself does not
6
+ * scroll — the rail scrolls through the changes, the stage scrolls through the
7
+ * one that is open, and the address says which. That is the difference between a
8
+ * service and a report: a report is a column of everything, read top to bottom
9
+ * once; a service is a place a reviewer navigates, comes back to, sends a link
10
+ * into, and leaves a browser tab on.
11
+ *
12
+ * The four addresses this page answers are four stages behind one rail:
13
+ *
14
+ * - **`/builds/:id`** — the rail with nothing open, and a short reading of the
15
+ * run beside it. Not the encyclopedia: that is `/run`, and it is a link.
16
+ * - **`/builds/:id/changes/:component`** — one change, whole, with its own
17
+ * picture and its own two buttons.
18
+ * - **`/builds/:id/subjects/:subject`** — one render, whole, with the rail
19
+ * switched to subjects.
20
+ * - **`/builds/:id/run`** — everything the run read, full width.
21
+ *
22
+ * The crossing against the previous build is fetched **here**, once, and handed
23
+ * to all four. It is what lets a change card say *build 5 showed you this and
24
+ * nobody decided it* without a second request per card, and it is the reason the
25
+ * run page and a change can never disagree about what the last run said.
26
+ */
27
+ import { useCallback, useEffect, useState } from 'react';
28
+ import { ChangePanel } from './change.js';
29
+ import { useCrossing } from './crossing.js';
30
+ import { distanceFrom } from './distance.js';
31
+ import { Impact } from './impact.js';
32
+ import { originsOf } from './grouping.js';
33
+ import { causeOf } from './lead.js';
34
+ import { OriginsPanel } from './origins.js';
35
+ import { SubjectRail } from './rail.js';
36
+ import { RunPage } from './run.js';
37
+ import { needsReview } from './settled.js';
38
+ import { Failure, Go, Topbar, messageOf } from './shell.js';
39
+ import { SubjectPanel } from './subject.js';
40
+ import { count, number } from './text.js';
41
+ export function BuildPage({ client, reviewer, route, go, }) {
42
+ const [detail, setDetail] = useState({ state: 'loading' });
43
+ const load = useCallback(async () => {
44
+ try {
45
+ setDetail({ state: 'ready', value: await client.build(route.build) });
46
+ }
47
+ catch (error) {
48
+ setDetail({ state: 'failed', why: messageOf(error) });
49
+ }
50
+ }, [client, route.build]);
51
+ useEffect(() => {
52
+ void load();
53
+ }, [load]);
54
+ if (detail.state === 'loading') {
55
+ return (_jsx("div", { className: "va-app", children: _jsxs("p", { className: "va-note", children: ["Loading ", route.build, "\u2026"] }) }));
56
+ }
57
+ if (detail.state === 'failed') {
58
+ return (_jsx("div", { className: "va-app", children: _jsx(Failure, { why: detail.why, retry: load }) }));
59
+ }
60
+ return (_jsx(Build, { client: client, reviewer: reviewer, build: detail.value, route: route, go: go, reload: load }));
61
+ }
62
+ /**
63
+ * The build, once it is loaded — and the level the crossing is asked at.
64
+ *
65
+ * Split from {@link BuildPage} because {@link useCrossing} needs a build to cross
66
+ * and hooks cannot be called behind a loading branch.
67
+ */
68
+ function Build({ client, reviewer, build, route, go, reload, }) {
69
+ const crossing = useCrossing(client, build);
70
+ const order = (route.page === 'build' ? route.order : undefined) ?? 'story';
71
+ const { origins } = originsOf(build);
72
+ const reviewable = build.subjects.filter((subject) => needsReview(subject.verdict));
73
+ const open = route.page === 'change' ? origins.find((each) => each.component === route.change) : undefined;
74
+ const subject = route.page === 'subject'
75
+ ? build.subjects.find((each) => each.subject === route.subject)
76
+ : undefined;
77
+ // Anchored on the change this render is filed under: on a page with no single
78
+ // change, distance has nothing to be measured from.
79
+ const anchor = subject === undefined ? undefined : causeOf(subject);
80
+ return (_jsxs("div", { className: "va-app", children: [_jsxs(Topbar, { crumbs: [{ at: { page: 'builds' }, say: 'Builds' }], go: go, title: `Build ${build.build}`, subtitle: build.project, children: [_jsx("span", { className: build.pending > 0 ? 'va-pill va-warn' : 'va-pill va-good', children: build.pending > 0 ? `${number(build.pending)} awaiting review` : 'settled' }), _jsxs("span", { className: "va-topbar-meta", children: [_jsx("code", { className: "va-commit", children: build.commit.slice(0, 8) }), build.branch === undefined ? null : _jsx("span", { children: build.branch }), _jsx("span", { children: build.identity.engine }), _jsx(Go, { to: { page: 'run', build: build.build }, go: go, className: "va-mode", children: "The run" })] })] }), route.page === 'run' ? (_jsx("div", { className: "va-body", children: _jsx(RunPage, { client: client, build: build, crossing: crossing }) })) : (_jsxs("div", { className: "va-body", children: [_jsxs("nav", { className: "va-rail", children: [_jsx(Switch, { build: build.build, order: order, subjects: reviewable, route: route, go: go }), route.page === 'subject' ? (_jsx(SubjectRail, { subjects: reviewable, causes: build.causes.length, variations: build.variations.length, selected: route.subject, onSelect: (next) => go(next === null
81
+ ? { page: 'build', build: build.build }
82
+ : { page: 'subject', build: build.build, subject: next }) })) : (_jsx(OriginsPanel, { build: build, crossing: crossing, order: order, selected: open?.component, go: go }))] }), subject !== undefined ? (_jsx(SubjectPanel, { client: client, reviewer: reviewer, build: build.build, subject: subject, ...(anchor === undefined ? {} : { anchor, far: distanceFrom(build, anchor) }), sourced: build.causes.some((cause) => cause.file !== undefined), onDecided: reload })) : open !== undefined ? (_jsx(ChangePanel, { client: client, reviewer: reviewer, build: build, origin: open, crossing: crossing, changes: new Set(origins.map((each) => each.component)), go: go, onDecided: reload })) : (_jsx(Opening, { build: build, crossing: crossing, route: route, go: go }))] }))] }));
83
+ }
84
+ /** Changes or subjects: the two ways to work a build, as two addresses. */
85
+ function Switch({ build, order, subjects, route, go, }) {
86
+ const first = subjects[0]?.subject;
87
+ return (_jsxs("div", { className: "va-switch", role: "group", "aria-label": "What to work through", children: [_jsx(Go, { to: { page: 'build', build, order }, go: go, className: route.page === 'subject' ? 'va-mode' : 'va-mode va-on', title: "One row per change: a component and everywhere it moved", children: "Changes" }), first === undefined ? (_jsx("span", { className: "va-mode va-off", title: "Nothing in this build is awaiting a decision", children: "Subjects" })) : (_jsx(Go, { to: { page: 'subject', build, subject: route.page === 'subject' ? route.subject : first }, go: go, className: route.page === 'subject' ? 'va-mode va-on' : 'va-mode', title: "One row per render, which is what the run measured", children: "Subjects" }))] }));
88
+ }
89
+ /**
90
+ * The stage with nothing open: a short reading of the build, and where to go.
91
+ *
92
+ * Everything a reader could want past this has an address, and putting it here
93
+ * instead is how the previous version of this page came to be read top to bottom
94
+ * by nobody.
95
+ *
96
+ * Under the heading there used to be a definition — *a change is a component and
97
+ * every render it moved in, one decision covers all of them* — printed on every
98
+ * build of every project, in the position a reader gives their second glance to.
99
+ * It is true and it is the same sentence every time, which makes it the most
100
+ * expensive line on the page. The heading already counts the changes; the tally,
101
+ * the commits and the diff below are about this build.
102
+ */
103
+ function Opening({ build, crossing, route, go, }) {
104
+ const { origins, unattributed } = originsOf(build);
105
+ const missing = route.page === 'change' ? route.change : undefined;
106
+ return (_jsx("div", { className: "va-stage va-scroll", children: _jsxs("div", { className: "va-page va-opening", children: [missing === undefined ? null : (_jsxs("p", { className: "va-failure", children: ["This build has no change under ", _jsx("strong", { children: missing }), ". It may have been decided and the baseline promoted, or the link may be from another build."] })), _jsx("h1", { children: origins.length === 0
107
+ ? 'Nothing in this build changed'
108
+ : `${count(origins.length, 'change')} to work through` }), origins.length === 0 ? _jsx(Still, { verdicts: build.verdicts }) : null, unattributed.length === 0 ? null : (_jsxs("p", { className: "va-note", children: [count(unattributed.length, 'render'), " moved with no component named as the cause by a region."] })), _jsx(Since, { crossing: crossing }), _jsx(Impact, { build: build, crossing: crossing }), _jsxs("p", { className: "va-opening-links", children: [_jsx(Go, { to: { page: 'run', build: build.build }, go: go, className: "va-mode", children: "What this run read \u2192" }), _jsx("span", { className: "va-note", children: "Reach, the crossing in full, the variation lattice, and what nothing looked at." })] })] }) }));
109
+ }
110
+ /**
111
+ * What a build with no changes did instead, counted.
112
+ *
113
+ * *Every subject matched its baseline, or was settled by a rule you wrote* was
114
+ * the line here, and the `or` is the whole problem: the two halves are different
115
+ * builds — one where the suite is green and one where a ledger is absorbing
116
+ * differences — and the store knows which. A reader who wrote an ignore rule last
117
+ * week and wants to know whether it is still swallowing something was being told
118
+ * the two possibilities they already knew about.
119
+ *
120
+ * Zeroes are dropped rather than printed, because a clean build reads as *41
121
+ * unchanged* and not as a row of noughts to scan past.
122
+ */
123
+ function Still({ verdicts }) {
124
+ const said = [
125
+ verdicts.unchanged === 0 ? undefined : `${number(verdicts.unchanged)} unchanged`,
126
+ verdicts.ignored === 0 ? undefined : `${number(verdicts.ignored)} settled by a rule`,
127
+ verdicts.new === 0 ? undefined : `${count(verdicts.new, 'new subject')}`,
128
+ verdicts.incomparable === 0 ? undefined : `${count(verdicts.incomparable, 'incomparable')}`,
129
+ ].filter((clause) => clause !== undefined);
130
+ if (said.length === 0)
131
+ return null;
132
+ return _jsx("p", { className: "va-subtitle", children: said.join(' · ') });
133
+ }
134
+ /** The build against the one before it, in one line, on the page it opens on. */
135
+ function Since({ crossing }) {
136
+ if (crossing.state === 'loading')
137
+ return _jsx("p", { className: "va-note", children: "Reading the previous run\u2026" });
138
+ if (crossing.state === 'none') {
139
+ return _jsx("p", { className: "va-note", children: "There is no earlier run to place this one against." });
140
+ }
141
+ if (crossing.state === 'failed') {
142
+ return (_jsxs("p", { className: "va-failure", children: ["The previous run could not be read, so nothing here says what has changed since it.", ' ', crossing.why] }));
143
+ }
144
+ const { shifts, held } = crossing.divergence;
145
+ const again = shifts.filter((each) => each.shift === 'again').length;
146
+ const fresh = shifts.filter((each) => each.shift === 'new' || each.shift === 'first').length;
147
+ const clauses = [
148
+ again === 0
149
+ ? undefined
150
+ : `${count(again, 'subject')} ${again === 1 ? 'carries a difference it' : 'carry a difference they'} already had`,
151
+ fresh === 0 ? undefined : `${count(fresh, 'subject')} moved for the first time`,
152
+ held === 0 ? undefined : `${count(held, 'subject')} held still in both`,
153
+ ].filter((each) => each !== undefined);
154
+ if (clauses.length === 0) {
155
+ return (_jsxs("p", { className: "va-since", children: ["Against build ", crossing.earlier.build, ": nothing here is a difference it also carried, and nothing held still in both. Every subject reads some other way than it did."] }));
156
+ }
157
+ return (_jsxs("p", { className: "va-since", children: ["Against build ", crossing.earlier.build, ": ", joined(clauses), ".", again === 0 ? '' : ' You have seen those.'] }));
158
+ }
159
+ /**
160
+ * The clauses as one sentence, in the order they were built.
161
+ *
162
+ * Only the ones with something in them. A count of zero read as *0 moved for the
163
+ * first time* on a run where nothing was new, and a reader has to work out that a
164
+ * number they were shown means the thing did not happen — absent is not zero, and
165
+ * a clause about nothing is a clause worth dropping.
166
+ */
167
+ function joined(clauses) {
168
+ if (clauses.length === 1)
169
+ return clauses[0] ?? '';
170
+ return `${clauses.slice(0, -1).join(', ')} and ${clauses[clauses.length - 1] ?? ''}`;
171
+ }
172
+ //# sourceMappingURL=docket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docket.js","sourceRoot":"","sources":["../../src/ui/docket.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * A build: the docket on the left, one thing open beside it.\n *\n * Two panes, and neither of them is the document. The page itself does not\n * scroll — the rail scrolls through the changes, the stage scrolls through the\n * one that is open, and the address says which. That is the difference between a\n * service and a report: a report is a column of everything, read top to bottom\n * once; a service is a place a reviewer navigates, comes back to, sends a link\n * into, and leaves a browser tab on.\n *\n * The four addresses this page answers are four stages behind one rail:\n *\n * - **`/builds/:id`** — the rail with nothing open, and a short reading of the\n * run beside it. Not the encyclopedia: that is `/run`, and it is a link.\n * - **`/builds/:id/changes/:component`** — one change, whole, with its own\n * picture and its own two buttons.\n * - **`/builds/:id/subjects/:subject`** — one render, whole, with the rail\n * switched to subjects.\n * - **`/builds/:id/run`** — everything the run read, full width.\n *\n * The crossing against the previous build is fetched **here**, once, and handed\n * to all four. It is what lets a change card say *build 5 showed you this and\n * nobody decided it* without a second request per card, and it is the reason the\n * run page and a change can never disagree about what the last run said.\n */\n\nimport { useCallback, useEffect, useState, type ReactElement } from 'react';\nimport type { BuildDetail } from '../review-types.js';\nimport { ChangePanel } from './change.js';\nimport type { ReviewClient } from './client.js';\nimport { useCrossing, type Crossing } from './crossing.js';\nimport { distanceFrom } from './distance.js';\nimport { Impact } from './impact.js';\nimport { originsOf } from './grouping.js';\nimport { causeOf } from './lead.js';\nimport { OriginsPanel } from './origins.js';\nimport { SubjectRail } from './rail.js';\nimport type { Order, Route } from './route.js';\nimport { RunPage } from './run.js';\nimport { needsReview } from './settled.js';\nimport { Failure, Go, Topbar, messageOf, type Loaded } from './shell.js';\nimport { SubjectPanel } from './subject.js';\nimport { count, number } from './text.js';\n\n/** The four addresses that live inside one build. */\nexport type BuildRoute = Extract<Route, { page: 'build' | 'change' | 'subject' | 'run' }>;\n\nexport function BuildPage({\n client,\n reviewer,\n route,\n go,\n}: {\n readonly client: ReviewClient;\n readonly reviewer: string;\n readonly route: BuildRoute;\n readonly go: (route: Route) => void;\n}): ReactElement {\n const [detail, setDetail] = useState<Loaded<BuildDetail>>({ state: 'loading' });\n\n const load = useCallback(async (): Promise<void> => {\n try {\n setDetail({ state: 'ready', value: await client.build(route.build) });\n } catch (error) {\n setDetail({ state: 'failed', why: messageOf(error) });\n }\n }, [client, route.build]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n if (detail.state === 'loading') {\n return (\n <div className=\"va-app\">\n <p className=\"va-note\">Loading {route.build}…</p>\n </div>\n );\n }\n if (detail.state === 'failed') {\n return (\n <div className=\"va-app\">\n <Failure why={detail.why} retry={load} />\n </div>\n );\n }\n\n return (\n <Build client={client} reviewer={reviewer} build={detail.value} route={route} go={go} reload={load} />\n );\n}\n\n/**\n * The build, once it is loaded — and the level the crossing is asked at.\n *\n * Split from {@link BuildPage} because {@link useCrossing} needs a build to cross\n * and hooks cannot be called behind a loading branch.\n */\nfunction Build({\n client,\n reviewer,\n build,\n route,\n go,\n reload,\n}: {\n readonly client: ReviewClient;\n readonly reviewer: string;\n readonly build: BuildDetail;\n readonly route: BuildRoute;\n readonly go: (route: Route) => void;\n readonly reload: () => void;\n}): ReactElement {\n const crossing = useCrossing(client, build);\n const order: Order = (route.page === 'build' ? route.order : undefined) ?? 'story';\n\n const { origins } = originsOf(build);\n const reviewable = build.subjects.filter((subject) => needsReview(subject.verdict));\n const open = route.page === 'change' ? origins.find((each) => each.component === route.change) : undefined;\n const subject =\n route.page === 'subject'\n ? build.subjects.find((each) => each.subject === route.subject)\n : undefined;\n // Anchored on the change this render is filed under: on a page with no single\n // change, distance has nothing to be measured from.\n const anchor = subject === undefined ? undefined : causeOf(subject);\n\n return (\n <div className=\"va-app\">\n <Topbar\n crumbs={[{ at: { page: 'builds' }, say: 'Builds' }]}\n go={go}\n title={`Build ${build.build}`}\n subtitle={build.project}\n >\n <span className={build.pending > 0 ? 'va-pill va-warn' : 'va-pill va-good'}>\n {build.pending > 0 ? `${number(build.pending)} awaiting review` : 'settled'}\n </span>\n <span className=\"va-topbar-meta\">\n <code className=\"va-commit\">{build.commit.slice(0, 8)}</code>\n {build.branch === undefined ? null : <span>{build.branch}</span>}\n <span>{build.identity.engine}</span>\n <Go to={{ page: 'run', build: build.build }} go={go} className=\"va-mode\">\n The run\n </Go>\n </span>\n </Topbar>\n\n {route.page === 'run' ? (\n <div className=\"va-body\">\n <RunPage client={client} build={build} crossing={crossing} />\n </div>\n ) : (\n <div className=\"va-body\">\n <nav className=\"va-rail\">\n <Switch build={build.build} order={order} subjects={reviewable} route={route} go={go} />\n {route.page === 'subject' ? (\n <SubjectRail\n subjects={reviewable}\n causes={build.causes.length}\n variations={build.variations.length}\n selected={route.subject}\n onSelect={(next) =>\n go(\n next === null\n ? { page: 'build', build: build.build }\n : { page: 'subject', build: build.build, subject: next },\n )\n }\n />\n ) : (\n <OriginsPanel\n build={build}\n crossing={crossing}\n order={order}\n selected={open?.component}\n go={go}\n />\n )}\n </nav>\n\n {subject !== undefined ? (\n <SubjectPanel\n client={client}\n reviewer={reviewer}\n build={build.build}\n subject={subject}\n {...(anchor === undefined ? {} : { anchor, far: distanceFrom(build, anchor) })}\n sourced={build.causes.some((cause) => cause.file !== undefined)}\n onDecided={reload}\n />\n ) : open !== undefined ? (\n <ChangePanel\n client={client}\n reviewer={reviewer}\n build={build}\n origin={open}\n crossing={crossing}\n changes={new Set(origins.map((each) => each.component))}\n go={go}\n onDecided={reload}\n />\n ) : (\n <Opening build={build} crossing={crossing} route={route} go={go} />\n )}\n </div>\n )}\n </div>\n );\n}\n\n/** Changes or subjects: the two ways to work a build, as two addresses. */\nfunction Switch({\n build,\n order,\n subjects,\n route,\n go,\n}: {\n readonly build: string;\n readonly order: Order;\n readonly subjects: readonly { readonly subject: string }[];\n readonly route: BuildRoute;\n readonly go: (route: Route) => void;\n}): ReactElement {\n const first = subjects[0]?.subject;\n\n return (\n <div className=\"va-switch\" role=\"group\" aria-label=\"What to work through\">\n <Go\n to={{ page: 'build', build, order }}\n go={go}\n className={route.page === 'subject' ? 'va-mode' : 'va-mode va-on'}\n title=\"One row per change: a component and everywhere it moved\"\n >\n Changes\n </Go>\n {first === undefined ? (\n <span className=\"va-mode va-off\" title=\"Nothing in this build is awaiting a decision\">\n Subjects\n </span>\n ) : (\n <Go\n to={{ page: 'subject', build, subject: route.page === 'subject' ? route.subject : first }}\n go={go}\n className={route.page === 'subject' ? 'va-mode va-on' : 'va-mode'}\n title=\"One row per render, which is what the run measured\"\n >\n Subjects\n </Go>\n )}\n </div>\n );\n}\n\n/**\n * The stage with nothing open: a short reading of the build, and where to go.\n *\n * Everything a reader could want past this has an address, and putting it here\n * instead is how the previous version of this page came to be read top to bottom\n * by nobody.\n *\n * Under the heading there used to be a definition — *a change is a component and\n * every render it moved in, one decision covers all of them* — printed on every\n * build of every project, in the position a reader gives their second glance to.\n * It is true and it is the same sentence every time, which makes it the most\n * expensive line on the page. The heading already counts the changes; the tally,\n * the commits and the diff below are about this build.\n */\nfunction Opening({\n build,\n crossing,\n route,\n go,\n}: {\n readonly build: BuildDetail;\n readonly crossing: Crossing;\n readonly route: BuildRoute;\n readonly go: (route: Route) => void;\n}): ReactElement {\n const { origins, unattributed } = originsOf(build);\n const missing = route.page === 'change' ? route.change : undefined;\n\n return (\n <div className=\"va-stage va-scroll\">\n <div className=\"va-page va-opening\">\n {missing === undefined ? null : (\n <p className=\"va-failure\">\n This build has no change under <strong>{missing}</strong>. It may have been decided and\n the baseline promoted, or the link may be from another build.\n </p>\n )}\n\n <h1>\n {origins.length === 0\n ? 'Nothing in this build changed'\n : `${count(origins.length, 'change')} to work through`}\n </h1>\n {origins.length === 0 ? <Still verdicts={build.verdicts} /> : null}\n\n {unattributed.length === 0 ? null : (\n <p className=\"va-note\">\n {count(unattributed.length, 'render')} moved with no component named as the cause by a\n region.\n </p>\n )}\n\n <Since crossing={crossing} />\n <Impact build={build} crossing={crossing} />\n\n <p className=\"va-opening-links\">\n <Go to={{ page: 'run', build: build.build }} go={go} className=\"va-mode\">\n What this run read →\n </Go>\n <span className=\"va-note\">\n Reach, the crossing in full, the variation lattice, and what nothing looked at.\n </span>\n </p>\n </div>\n </div>\n );\n}\n\n/**\n * What a build with no changes did instead, counted.\n *\n * *Every subject matched its baseline, or was settled by a rule you wrote* was\n * the line here, and the `or` is the whole problem: the two halves are different\n * builds — one where the suite is green and one where a ledger is absorbing\n * differences — and the store knows which. A reader who wrote an ignore rule last\n * week and wants to know whether it is still swallowing something was being told\n * the two possibilities they already knew about.\n *\n * Zeroes are dropped rather than printed, because a clean build reads as *41\n * unchanged* and not as a row of noughts to scan past.\n */\nfunction Still({ verdicts }: { readonly verdicts: BuildDetail['verdicts'] }): ReactElement | null {\n const said = [\n verdicts.unchanged === 0 ? undefined : `${number(verdicts.unchanged)} unchanged`,\n verdicts.ignored === 0 ? undefined : `${number(verdicts.ignored)} settled by a rule`,\n verdicts.new === 0 ? undefined : `${count(verdicts.new, 'new subject')}`,\n verdicts.incomparable === 0 ? undefined : `${count(verdicts.incomparable, 'incomparable')}`,\n ].filter((clause): clause is string => clause !== undefined);\n\n if (said.length === 0) return null;\n return <p className=\"va-subtitle\">{said.join(' · ')}</p>;\n}\n\n/** The build against the one before it, in one line, on the page it opens on. */\nfunction Since({ crossing }: { readonly crossing: Crossing }): ReactElement | null {\n if (crossing.state === 'loading') return <p className=\"va-note\">Reading the previous run…</p>;\n if (crossing.state === 'none') {\n return <p className=\"va-note\">There is no earlier run to place this one against.</p>;\n }\n if (crossing.state === 'failed') {\n return (\n <p className=\"va-failure\">\n The previous run could not be read, so nothing here says what has changed since it.{' '}\n {crossing.why}\n </p>\n );\n }\n\n const { shifts, held } = crossing.divergence;\n const again = shifts.filter((each) => each.shift === 'again').length;\n const fresh = shifts.filter((each) => each.shift === 'new' || each.shift === 'first').length;\n\n const clauses = [\n again === 0\n ? undefined\n : `${count(again, 'subject')} ${again === 1 ? 'carries a difference it' : 'carry a difference they'} already had`,\n fresh === 0 ? undefined : `${count(fresh, 'subject')} moved for the first time`,\n held === 0 ? undefined : `${count(held, 'subject')} held still in both`,\n ].filter((each): each is string => each !== undefined);\n\n if (clauses.length === 0) {\n return (\n <p className=\"va-since\">\n Against build {crossing.earlier.build}: nothing here is a difference it also carried, and\n nothing held still in both. Every subject reads some other way than it did.\n </p>\n );\n }\n\n return (\n <p className=\"va-since\">\n Against build {crossing.earlier.build}: {joined(clauses)}.\n {again === 0 ? '' : ' You have seen those.'}\n </p>\n );\n}\n\n/**\n * The clauses as one sentence, in the order they were built.\n *\n * Only the ones with something in them. A count of zero read as *0 moved for the\n * first time* on a run where nothing was new, and a reader has to work out that a\n * number they were shown means the thing did not happen — absent is not zero, and\n * a clause about nothing is a clause worth dropping.\n */\nfunction joined(clauses: readonly string[]): string {\n if (clauses.length === 1) return clauses[0] ?? '';\n return `${clauses.slice(0, -1).join(', ')} and ${clauses[clauses.length - 1] ?? ''}`;\n}\n"]}
@@ -0,0 +1,42 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { SubjectView } from '../review.js';
3
+ /**
4
+ * Findings, and the three things a reader has to be told before the list is one.
5
+ *
6
+ * **Whether anything looked.** `[]` means this render was inspected and no defect
7
+ * was found. `undefined` means nothing inspected it. Printing the second as the
8
+ * first tells a reviewer the component is fine on the authority of something that
9
+ * never looked.
10
+ *
11
+ * **What kind of defect these are.** The rows used to arrive under a bare
12
+ * *Findings* as rule ids and clauses, and a reader who did not already know what
13
+ * the inspector checks had to infer the subject of the report from the names of
14
+ * its rules. The band is on the record, so the list is grouped by it and each
15
+ * group is headed with the word — `Accessibility` over the nine rules that are,
16
+ * and not over the two that are not.
17
+ *
18
+ * **Whether it is theirs, and therefore what comes first.** A finding is read
19
+ * from one render with no baseline consulted, so the same list prints on the run
20
+ * that introduced a defect and on every run after it. The panel was silent about
21
+ * which, then it said both in one breath — *42 defects read from this render ·
22
+ * 20 arrived with this change · 22 already in the baseline* — which is a sentence
23
+ * that opens on a number nobody can act on and gives a reviewer twenty-two
24
+ * afternoons of somebody else's work to scroll past on the way to their own. Now
25
+ * the defects this change brought lead the panel at full size, and everything the
26
+ * record does not put on this change is folded behind its count. *Not dated* is
27
+ * its own answer and is never drawn as *arrived with this change*: the reading is
28
+ * [`byArrival`](../../../report/src/findings.ts), shared with the HTML report so
29
+ * the two surfaces say it in the same words and in the same order.
30
+ *
31
+ * Drawn in three registers rather than one line. The report writes a rule id and
32
+ * a clause — and the clause is written to *follow a noun the report never
33
+ * prints*, which is how `label-mismatch reads "SNKR. shop" and is named …` used
34
+ * to reach this page. So the headline is the defect in a person's words, the
35
+ * element the clause was written for comes from `where`, and the id goes last,
36
+ * beside the file, where it belongs: it is the least of the three to a reviewer
37
+ * and the whole of it to an ignore list.
38
+ */
39
+ export declare function Findings({ subject }: {
40
+ readonly subject: SubjectView;
41
+ }): ReactElement;
42
+ //# sourceMappingURL=findings.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AGE_WHY, AGE_WORDS, ageOf, arrivalLine, byArrival, byBand, carriedLine, methodLine, mixedAges, } from '@variance-authority/report';
3
+ import { element, headline, number, sentence } from './text.js';
4
+ /**
5
+ * Findings, and the three things a reader has to be told before the list is one.
6
+ *
7
+ * **Whether anything looked.** `[]` means this render was inspected and no defect
8
+ * was found. `undefined` means nothing inspected it. Printing the second as the
9
+ * first tells a reviewer the component is fine on the authority of something that
10
+ * never looked.
11
+ *
12
+ * **What kind of defect these are.** The rows used to arrive under a bare
13
+ * *Findings* as rule ids and clauses, and a reader who did not already know what
14
+ * the inspector checks had to infer the subject of the report from the names of
15
+ * its rules. The band is on the record, so the list is grouped by it and each
16
+ * group is headed with the word — `Accessibility` over the nine rules that are,
17
+ * and not over the two that are not.
18
+ *
19
+ * **Whether it is theirs, and therefore what comes first.** A finding is read
20
+ * from one render with no baseline consulted, so the same list prints on the run
21
+ * that introduced a defect and on every run after it. The panel was silent about
22
+ * which, then it said both in one breath — *42 defects read from this render ·
23
+ * 20 arrived with this change · 22 already in the baseline* — which is a sentence
24
+ * that opens on a number nobody can act on and gives a reviewer twenty-two
25
+ * afternoons of somebody else's work to scroll past on the way to their own. Now
26
+ * the defects this change brought lead the panel at full size, and everything the
27
+ * record does not put on this change is folded behind its count. *Not dated* is
28
+ * its own answer and is never drawn as *arrived with this change*: the reading is
29
+ * [`byArrival`](../../../report/src/findings.ts), shared with the HTML report so
30
+ * the two surfaces say it in the same words and in the same order.
31
+ *
32
+ * Drawn in three registers rather than one line. The report writes a rule id and
33
+ * a clause — and the clause is written to *follow a noun the report never
34
+ * prints*, which is how `label-mismatch reads "SNKR. shop" and is named …` used
35
+ * to reach this page. So the headline is the defect in a person's words, the
36
+ * element the clause was written for comes from `where`, and the id goes last,
37
+ * beside the file, where it belongs: it is the least of the three to a reviewer
38
+ * and the whole of it to an ignore list.
39
+ */
40
+ export function Findings({ subject }) {
41
+ if (subject.findings === undefined) {
42
+ return _jsx("p", { className: "va-note", children: "This render was not inspected, so no defect list applies." });
43
+ }
44
+ if (subject.findings.length === 0) {
45
+ return _jsx("p", { className: "va-note", children: "Inspected, and nothing to report." });
46
+ }
47
+ const { arrived, rest, dated } = byArrival(subject.findings);
48
+ const carried = carriedLine(subject.findings);
49
+ return (_jsxs(_Fragment, { children: [_jsx("p", { className: arrived.length > 0 ? 'va-findings-lead va-findings-mine' : 'va-findings-lead', children: arrivalLine(subject.findings) }), arrived.length === 0 ? null : _jsx(Bands, { findings: arrived }), dated ? null : _jsx(Bands, { findings: rest }), carried === undefined ? null : (_jsxs("details", { className: "va-findings-rest", children: [_jsx("summary", { children: carried }), _jsx("p", { className: "va-note", children: AGE_WHY[rest.some((each) => ageOf(each) === 'standing') ? 'standing' : 'undated'] }), _jsx(Bands, { findings: rest })] })), _jsx("p", { className: "va-note va-findings-why", children: methodLine(subject.findings) })] }));
50
+ }
51
+ /** One list, banded. The same markup whichever side of the fold it is drawn on. */
52
+ function Bands({ findings }) {
53
+ const dated = mixedAges(findings);
54
+ return (_jsx(_Fragment, { children: byBand(findings).map((group) => (_jsxs("section", { className: "va-findings-band", children: [_jsx("h3", { className: "va-band-head", children: group.title }), _jsx("ul", { className: "va-findings", children: occurrences(group.findings).map(({ key, finding, times }) => (_jsx(Row, { finding: finding, times: times, dated: dated }, key))) })] }, group.title))) }));
55
+ }
56
+ /**
57
+ * One defect, ruled by its date, and worded only when its neighbours differ.
58
+ *
59
+ * The colour is unconditional: a reviewer scanning a panel of fourteen reads the
60
+ * edge before they read anything, and what they are scanning for is the one that
61
+ * arrived with their change — so *arrived* is the only state that gets the loud
62
+ * edge, and the two that are not a claim about this change share the quiet one.
63
+ *
64
+ * The phrase is not. The list a row sits in is now split by date, so the heading
65
+ * above it already said *arrived with this change* once, and repeating it down
66
+ * fourteen rows is a word a reader stops seeing. `dated` is the one case where
67
+ * the row is carrying the finding instead of echoing the heading: a fold holding
68
+ * both a defect the baseline had and one nothing dated.
69
+ */
70
+ function Row({ finding, times, dated, }) {
71
+ const age = ageOf(finding);
72
+ return (_jsxs("li", { className: `va-finding va-finding-${age}`, children: [_jsxs("p", { className: "va-finding-title", children: [_jsx("span", { className: "va-finding-said", children: headline(finding.rule) }), _jsxs("span", { className: "va-finding-marks", children: [times > 1 ? _jsxs("span", { className: "va-times", children: [number(times), " places"] }) : null, dated ? (_jsx("span", { className: `va-age va-age-${age}`, title: AGE_WHY[age], children: AGE_WORDS[age] })) : null] })] }), _jsx("p", { className: "va-finding-where", children: element(finding.where) ?? finding.path }), _jsx("p", { className: "va-finding-what", children: sentence(finding.what) }), _jsxs("p", { className: "va-finding-owner", children: [finding.component === undefined ? null : (_jsx("span", { className: "va-tag", children: finding.component })), finding.file === undefined ? null : _jsx("code", { className: "va-tag", children: finding.file }), _jsx("span", { className: "va-tag va-rule", children: finding.rule })] })] }));
73
+ }
74
+ /**
75
+ * One defect, however many times the render draws the component carrying it.
76
+ *
77
+ * A listing page draws the same card twenty times, so a rule that fires on that
78
+ * card fires twenty times, and this panel printed twenty identical blocks: same
79
+ * rule, same element, same line of the same file, differing only in a DOM path
80
+ * no reviewer reads. That is not twenty decisions. It is one defect in `Button`,
81
+ * and the number worth printing is how far it reached.
82
+ *
83
+ * Grouped on everything the entry actually shows, which is all of a finding
84
+ * except its path. Two rows a reader cannot tell apart are one finding; `where`
85
+ * stays in the key, so two genuinely different elements stay two rows, and so
86
+ * does `standing` — a rule that fired on an element the baseline had and on one
87
+ * it did not is the case this panel exists to separate, and folding the two would
88
+ * report the new element under the old one's date. Insertion order is kept,
89
+ * because the report's order is the order of the render.
90
+ */
91
+ function occurrences(findings) {
92
+ const seen = new Map();
93
+ for (const finding of findings) {
94
+ const key = [
95
+ finding.rule,
96
+ finding.component ?? '',
97
+ finding.file ?? '',
98
+ finding.where ?? '',
99
+ finding.what,
100
+ ageOf(finding),
101
+ ].join(' · ');
102
+ const already = seen.get(key);
103
+ if (already === undefined)
104
+ seen.set(key, { key, finding, times: 1 });
105
+ else
106
+ already.times += 1;
107
+ }
108
+ return [...seen.values()];
109
+ }
110
+ //# sourceMappingURL=findings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings.js","sourceRoot":"","sources":["../../src/ui/findings.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,OAAO,EACP,SAAS,EACT,KAAK,EACL,WAAW,EACX,SAAS,EACT,MAAM,EACN,WAAW,EACX,UAAU,EACV,SAAS,GACV,MAAM,4BAA4B,CAAC;AAIpC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAE,OAAO,EAAqC;IACrE,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,YAAG,SAAS,EAAC,SAAS,0EAA8D,CAAC;IAC9F,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClC,OAAO,YAAG,SAAS,EAAC,SAAS,kDAAsC,CAAC;IACtE,CAAC;IAED,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7D,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE9C,OAAO,CACL,8BACE,YAAG,SAAS,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,mCAAmC,CAAC,CAAC,CAAC,kBAAkB,YACxF,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,GAC5B,EAEH,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAC,KAAK,IAAC,QAAQ,EAAE,OAAO,GAAI,EAC1D,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAC,KAAK,IAAC,QAAQ,EAAE,IAAI,GAAI,EAExC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC9B,mBAAS,SAAS,EAAC,kBAAkB,aACnC,4BAAU,OAAO,GAAW,EAC5B,YAAG,SAAS,EAAC,SAAS,YACnB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,GAChF,EACJ,KAAC,KAAK,IAAC,QAAQ,EAAE,IAAI,GAAI,IACjB,CACX,EAED,YAAG,SAAS,EAAC,yBAAyB,YAAE,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAK,IACxE,CACJ,CAAC;AACJ,CAAC;AAED,mFAAmF;AACnF,SAAS,KAAK,CAAC,EAAE,QAAQ,EAAmD;IAC1E,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IAElC,OAAO,CACL,4BACG,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAC/B,mBAA2B,SAAS,EAAC,kBAAkB,aACrD,aAAI,SAAS,EAAC,cAAc,YAAE,KAAK,CAAC,KAAK,GAAM,EAC/C,aAAI,SAAS,EAAC,aAAa,YACxB,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAC5D,KAAC,GAAG,IAAW,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,IAAjD,GAAG,CAAkD,CAChE,CAAC,GACC,KANO,KAAK,CAAC,KAAK,CAOf,CACX,CAAC,GACD,CACJ,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,GAAG,CAAC,EACX,OAAO,EACP,KAAK,EACL,KAAK,GAMN;IACC,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IAE3B,OAAO,CACL,cAAI,SAAS,EAAE,yBAAyB,GAAG,EAAE,aAC3C,aAAG,SAAS,EAAC,kBAAkB,aAC7B,eAAM,SAAS,EAAC,iBAAiB,YAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,GAAQ,EACjE,gBAAM,SAAS,EAAC,kBAAkB,aAC/B,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,gBAAM,SAAS,EAAC,UAAU,aAAE,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,EAC3E,KAAK,CAAC,CAAC,CAAC,CACP,eAAM,SAAS,EAAE,iBAAiB,GAAG,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,YACzD,SAAS,CAAC,GAAG,CAAC,GACV,CACR,CAAC,CAAC,CAAC,IAAI,IACH,IACL,EACJ,YAAG,SAAS,EAAC,kBAAkB,YAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,GAAK,EAC5E,YAAG,SAAS,EAAC,iBAAiB,YAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,GAAK,EAC3D,aAAG,SAAS,EAAC,kBAAkB,aAC5B,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACxC,eAAM,SAAS,EAAC,QAAQ,YAAE,OAAO,CAAC,SAAS,GAAQ,CACpD,EACA,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,QAAQ,YAAE,OAAO,CAAC,IAAI,GAAQ,EACnF,eAAM,SAAS,EAAC,gBAAgB,YAAE,OAAO,CAAC,IAAI,GAAQ,IACpD,IACD,CACN,CAAC;AACJ,CAAC;AASD;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,WAAW,CAAC,QAAkC;IACrD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC3C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG;YACV,OAAO,CAAC,IAAI;YACZ,OAAO,CAAC,SAAS,IAAI,EAAE;YACvB,OAAO,CAAC,IAAI,IAAI,EAAE;YAClB,OAAO,CAAC,KAAK,IAAI,EAAE;YACnB,OAAO,CAAC,IAAI;YACZ,KAAK,CAAC,OAAO,CAAC;SACf,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACd,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,OAAO,KAAK,SAAS;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;;YAChE,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAC5B,CAAC","sourcesContent":["import {\n AGE_WHY,\n AGE_WORDS,\n ageOf,\n arrivalLine,\n byArrival,\n byBand,\n carriedLine,\n methodLine,\n mixedAges,\n} from '@variance-authority/report';\nimport type { FindingRecord } from '@variance-authority/report';\nimport type { ReactElement } from 'react';\nimport type { SubjectView } from '../review.js';\nimport { element, headline, number, sentence } from './text.js';\n\n/**\n * Findings, and the three things a reader has to be told before the list is one.\n *\n * **Whether anything looked.** `[]` means this render was inspected and no defect\n * was found. `undefined` means nothing inspected it. Printing the second as the\n * first tells a reviewer the component is fine on the authority of something that\n * never looked.\n *\n * **What kind of defect these are.** The rows used to arrive under a bare\n * *Findings* as rule ids and clauses, and a reader who did not already know what\n * the inspector checks had to infer the subject of the report from the names of\n * its rules. The band is on the record, so the list is grouped by it and each\n * group is headed with the word — `Accessibility` over the nine rules that are,\n * and not over the two that are not.\n *\n * **Whether it is theirs, and therefore what comes first.** A finding is read\n * from one render with no baseline consulted, so the same list prints on the run\n * that introduced a defect and on every run after it. The panel was silent about\n * which, then it said both in one breath — *42 defects read from this render ·\n * 20 arrived with this change · 22 already in the baseline* — which is a sentence\n * that opens on a number nobody can act on and gives a reviewer twenty-two\n * afternoons of somebody else's work to scroll past on the way to their own. Now\n * the defects this change brought lead the panel at full size, and everything the\n * record does not put on this change is folded behind its count. *Not dated* is\n * its own answer and is never drawn as *arrived with this change*: the reading is\n * [`byArrival`](../../../report/src/findings.ts), shared with the HTML report so\n * the two surfaces say it in the same words and in the same order.\n *\n * Drawn in three registers rather than one line. The report writes a rule id and\n * a clause — and the clause is written to *follow a noun the report never\n * prints*, which is how `label-mismatch reads \"SNKR. shop\" and is named …` used\n * to reach this page. So the headline is the defect in a person's words, the\n * element the clause was written for comes from `where`, and the id goes last,\n * beside the file, where it belongs: it is the least of the three to a reviewer\n * and the whole of it to an ignore list.\n */\nexport function Findings({ subject }: { readonly subject: SubjectView }): ReactElement {\n if (subject.findings === undefined) {\n return <p className=\"va-note\">This render was not inspected, so no defect list applies.</p>;\n }\n if (subject.findings.length === 0) {\n return <p className=\"va-note\">Inspected, and nothing to report.</p>;\n }\n\n const { arrived, rest, dated } = byArrival(subject.findings);\n const carried = carriedLine(subject.findings);\n\n return (\n <>\n <p className={arrived.length > 0 ? 'va-findings-lead va-findings-mine' : 'va-findings-lead'}>\n {arrivalLine(subject.findings)}\n </p>\n\n {arrived.length === 0 ? null : <Bands findings={arrived} />}\n {dated ? null : <Bands findings={rest} />}\n\n {carried === undefined ? null : (\n <details className=\"va-findings-rest\">\n <summary>{carried}</summary>\n <p className=\"va-note\">\n {AGE_WHY[rest.some((each) => ageOf(each) === 'standing') ? 'standing' : 'undated']}\n </p>\n <Bands findings={rest} />\n </details>\n )}\n\n <p className=\"va-note va-findings-why\">{methodLine(subject.findings)}</p>\n </>\n );\n}\n\n/** One list, banded. The same markup whichever side of the fold it is drawn on. */\nfunction Bands({ findings }: { readonly findings: readonly FindingRecord[] }): ReactElement {\n const dated = mixedAges(findings);\n\n return (\n <>\n {byBand(findings).map((group) => (\n <section key={group.title} className=\"va-findings-band\">\n <h3 className=\"va-band-head\">{group.title}</h3>\n <ul className=\"va-findings\">\n {occurrences(group.findings).map(({ key, finding, times }) => (\n <Row key={key} finding={finding} times={times} dated={dated} />\n ))}\n </ul>\n </section>\n ))}\n </>\n );\n}\n\n/**\n * One defect, ruled by its date, and worded only when its neighbours differ.\n *\n * The colour is unconditional: a reviewer scanning a panel of fourteen reads the\n * edge before they read anything, and what they are scanning for is the one that\n * arrived with their change — so *arrived* is the only state that gets the loud\n * edge, and the two that are not a claim about this change share the quiet one.\n *\n * The phrase is not. The list a row sits in is now split by date, so the heading\n * above it already said *arrived with this change* once, and repeating it down\n * fourteen rows is a word a reader stops seeing. `dated` is the one case where\n * the row is carrying the finding instead of echoing the heading: a fold holding\n * both a defect the baseline had and one nothing dated.\n */\nfunction Row({\n finding,\n times,\n dated,\n}: {\n readonly finding: FindingRecord;\n readonly times: number;\n /** Whether this list mixes dates, and so whether the row has to name its own. */\n readonly dated: boolean;\n}): ReactElement {\n const age = ageOf(finding);\n\n return (\n <li className={`va-finding va-finding-${age}`}>\n <p className=\"va-finding-title\">\n <span className=\"va-finding-said\">{headline(finding.rule)}</span>\n <span className=\"va-finding-marks\">\n {times > 1 ? <span className=\"va-times\">{number(times)} places</span> : null}\n {dated ? (\n <span className={`va-age va-age-${age}`} title={AGE_WHY[age]}>\n {AGE_WORDS[age]}\n </span>\n ) : null}\n </span>\n </p>\n <p className=\"va-finding-where\">{element(finding.where) ?? finding.path}</p>\n <p className=\"va-finding-what\">{sentence(finding.what)}</p>\n <p className=\"va-finding-owner\">\n {finding.component === undefined ? null : (\n <span className=\"va-tag\">{finding.component}</span>\n )}\n {finding.file === undefined ? null : <code className=\"va-tag\">{finding.file}</code>}\n <span className=\"va-tag va-rule\">{finding.rule}</span>\n </p>\n </li>\n );\n}\n\n/** One defect, and the number of times a single render repeats it. */\ninterface Occurrence {\n readonly key: string;\n readonly finding: FindingRecord;\n times: number;\n}\n\n/**\n * One defect, however many times the render draws the component carrying it.\n *\n * A listing page draws the same card twenty times, so a rule that fires on that\n * card fires twenty times, and this panel printed twenty identical blocks: same\n * rule, same element, same line of the same file, differing only in a DOM path\n * no reviewer reads. That is not twenty decisions. It is one defect in `Button`,\n * and the number worth printing is how far it reached.\n *\n * Grouped on everything the entry actually shows, which is all of a finding\n * except its path. Two rows a reader cannot tell apart are one finding; `where`\n * stays in the key, so two genuinely different elements stay two rows, and so\n * does `standing` — a rule that fired on an element the baseline had and on one\n * it did not is the case this panel exists to separate, and folding the two would\n * report the new element under the old one's date. Insertion order is kept,\n * because the report's order is the order of the render.\n */\nfunction occurrences(findings: readonly FindingRecord[]): readonly Occurrence[] {\n const seen = new Map<string, Occurrence>();\n for (const finding of findings) {\n const key = [\n finding.rule,\n finding.component ?? '',\n finding.file ?? '',\n finding.where ?? '',\n finding.what,\n ageOf(finding),\n ].join(' · ');\n const already = seen.get(key);\n if (already === undefined) seen.set(key, { key, finding, times: 1 });\n else already.times += 1;\n }\n return [...seen.values()];\n}\n"]}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * What the commit implied, set against what the run found — in both directions.
3
+ *
4
+ * A diff reaches subjects. That is a prediction, made before a single pixel was
5
+ * compared: *editing `ui/button.tsx` can arrive at these sixteen*. The run then
6
+ * compares them and finds fourteen moved. Nothing on the page has ever put those
7
+ * two numbers next to each other, and the gap between them is the only part of a
8
+ * build a reviewer cannot work out for themselves.
9
+ *
10
+ * ## The two directions fail differently
11
+ *
12
+ * **Reached and still** is the edit that did not land. Two `Button` stories are
13
+ * reached by the file the commit changed, were compared against a baseline, and
14
+ * nothing moved. Either the change does not do what its author thinks, or those
15
+ * two variants do not exercise it — and both are worth a reviewer's minute
16
+ * before they approve the fourteen that did move.
17
+ *
18
+ * **Changed and unreached** is the other way round, and it is the more serious
19
+ * one: something moved that the commit cannot explain. It is not attributed to a
20
+ * root, because attributing it would be the invention this module exists to
21
+ * catch, so it is reported against the build.
22
+ *
23
+ * ## Excluded is not still, and uncompared is not either
24
+ *
25
+ * The `MainNav` stories moved by three hundred pixels and every one of them fell
26
+ * inside a declared ignore. Filing them under *still* would tell a reviewer the
27
+ * edit missed the component it was written for, when in fact it hit it and a
28
+ * rule forgave it. And a subject with no baseline was never asked the question at
29
+ * all. Three outcomes, three lists, because a single count of *did not move*
30
+ * would be three findings averaged into one wrong one.
31
+ */
32
+ import type { BuildDetail } from '../review-types.js';
33
+ /** One changed file's prediction, and what became of it. */
34
+ export interface Foreseen {
35
+ /** Subjects the diff reaches through the components declared in this file. */
36
+ readonly reached: number;
37
+ /** Of those, the ones the run found changed. */
38
+ readonly moved: number;
39
+ /** Reached, compared, and nothing moved. The prediction that failed. */
40
+ readonly still: readonly string[];
41
+ /** Reached and moved, with every differing pixel inside a rule that forgives it. */
42
+ readonly excluded: readonly string[];
43
+ /** Reached and never compared — no baseline, or nothing to compare it against. */
44
+ readonly uncompared: readonly string[];
45
+ }
46
+ /**
47
+ * The prediction this file made, or `null` when the run made none.
48
+ *
49
+ * `null` covers both a build with no diff and a diff the run could not attribute
50
+ * to subjects. Zeroes would say the file reaches nothing, which is a finding, and
51
+ * this module must not manufacture one out of a missing input.
52
+ */
53
+ export declare function foreseenBy(build: BuildDetail, file: string): Foreseen | null;
54
+ export declare function unforeseen(build: BuildDetail): readonly string[] | null;
55
+ //# sourceMappingURL=foreseen.d.ts.map
@@ -0,0 +1,91 @@
1
+ /**
2
+ * What the commit implied, set against what the run found — in both directions.
3
+ *
4
+ * A diff reaches subjects. That is a prediction, made before a single pixel was
5
+ * compared: *editing `ui/button.tsx` can arrive at these sixteen*. The run then
6
+ * compares them and finds fourteen moved. Nothing on the page has ever put those
7
+ * two numbers next to each other, and the gap between them is the only part of a
8
+ * build a reviewer cannot work out for themselves.
9
+ *
10
+ * ## The two directions fail differently
11
+ *
12
+ * **Reached and still** is the edit that did not land. Two `Button` stories are
13
+ * reached by the file the commit changed, were compared against a baseline, and
14
+ * nothing moved. Either the change does not do what its author thinks, or those
15
+ * two variants do not exercise it — and both are worth a reviewer's minute
16
+ * before they approve the fourteen that did move.
17
+ *
18
+ * **Changed and unreached** is the other way round, and it is the more serious
19
+ * one: something moved that the commit cannot explain. It is not attributed to a
20
+ * root, because attributing it would be the invention this module exists to
21
+ * catch, so it is reported against the build.
22
+ *
23
+ * ## Excluded is not still, and uncompared is not either
24
+ *
25
+ * The `MainNav` stories moved by three hundred pixels and every one of them fell
26
+ * inside a declared ignore. Filing them under *still* would tell a reviewer the
27
+ * edit missed the component it was written for, when in fact it hit it and a
28
+ * rule forgave it. And a subject with no baseline was never asked the question at
29
+ * all. Three outcomes, three lists, because a single count of *did not move*
30
+ * would be three findings averaged into one wrong one.
31
+ */
32
+ /**
33
+ * The prediction this file made, or `null` when the run made none.
34
+ *
35
+ * `null` covers both a build with no diff and a diff the run could not attribute
36
+ * to subjects. Zeroes would say the file reaches nothing, which is a finding, and
37
+ * this module must not manufacture one out of a missing input.
38
+ */
39
+ export function foreseenBy(build, file) {
40
+ const per = build.reach?.subjects;
41
+ if (per === undefined)
42
+ return null;
43
+ const mine = new Set((build.reach?.components ?? [])
44
+ .filter((each) => each.trail[0] === file)
45
+ .map((each) => each.component));
46
+ if (mine.size === 0)
47
+ return null;
48
+ const verdicts = new Map(build.subjects.map((each) => [each.subject, each.verdict]));
49
+ const still = [];
50
+ const excluded = [];
51
+ const uncompared = [];
52
+ let reached = 0;
53
+ let moved = 0;
54
+ for (const [subject, how] of Object.entries(per)) {
55
+ if (!how.reached || !(how.through ?? []).some((each) => mine.has(each)))
56
+ continue;
57
+ reached += 1;
58
+ switch (verdicts.get(subject)) {
59
+ case 'changed':
60
+ moved += 1;
61
+ break;
62
+ case 'unchanged':
63
+ still.push(subject);
64
+ break;
65
+ case 'ignored':
66
+ excluded.push(subject);
67
+ break;
68
+ default:
69
+ // `new`, `incomparable`, and a subject the reach names that this build
70
+ // has no row for. None of them answered the question.
71
+ uncompared.push(subject);
72
+ }
73
+ }
74
+ return {
75
+ reached,
76
+ moved,
77
+ still: still.sort(),
78
+ excluded: excluded.sort(),
79
+ uncompared: uncompared.sort(),
80
+ };
81
+ }
82
+ export function unforeseen(build) {
83
+ const per = build.reach?.subjects;
84
+ if (per === undefined)
85
+ return null;
86
+ return build.subjects
87
+ .filter((each) => each.verdict === 'changed' && per[each.subject]?.reached !== true)
88
+ .map((each) => each.subject)
89
+ .sort();
90
+ }
91
+ //# sourceMappingURL=foreseen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foreseen.js","sourceRoot":"","sources":["../../src/ui/foreseen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAkBH;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,KAAkB,EAAE,IAAY;IACzD,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC;IAClC,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAEnC,MAAM,IAAI,GAAG,IAAI,GAAG,CAClB,CAAC,KAAK,CAAC,KAAK,EAAE,UAAU,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;SACxC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CACjC,CAAC;IACF,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEjC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACrF,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,KAAK,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACjD,IAAI,CAAC,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAAE,SAAS;QAClF,OAAO,IAAI,CAAC,CAAC;QAEb,QAAQ,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9B,KAAK,SAAS;gBACZ,KAAK,IAAI,CAAC,CAAC;gBACX,MAAM;YACR,KAAK,WAAW;gBACd,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACpB,MAAM;YACR,KAAK,SAAS;gBACZ,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACvB,MAAM;YACR;gBACE,uEAAuE;gBACvE,sDAAsD;gBACtD,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO;QACP,KAAK;QACL,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE;QACnB,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE;QACzB,UAAU,EAAE,UAAU,CAAC,IAAI,EAAE;KAC9B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAkB;IAC3C,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC;IAClC,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAEnC,OAAO,KAAK,CAAC,QAAQ;SAClB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;SACnF,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;SAC3B,IAAI,EAAE,CAAC;AACZ,CAAC","sourcesContent":["/**\n * What the commit implied, set against what the run found — in both directions.\n *\n * A diff reaches subjects. That is a prediction, made before a single pixel was\n * compared: *editing `ui/button.tsx` can arrive at these sixteen*. The run then\n * compares them and finds fourteen moved. Nothing on the page has ever put those\n * two numbers next to each other, and the gap between them is the only part of a\n * build a reviewer cannot work out for themselves.\n *\n * ## The two directions fail differently\n *\n * **Reached and still** is the edit that did not land. Two `Button` stories are\n * reached by the file the commit changed, were compared against a baseline, and\n * nothing moved. Either the change does not do what its author thinks, or those\n * two variants do not exercise it — and both are worth a reviewer's minute\n * before they approve the fourteen that did move.\n *\n * **Changed and unreached** is the other way round, and it is the more serious\n * one: something moved that the commit cannot explain. It is not attributed to a\n * root, because attributing it would be the invention this module exists to\n * catch, so it is reported against the build.\n *\n * ## Excluded is not still, and uncompared is not either\n *\n * The `MainNav` stories moved by three hundred pixels and every one of them fell\n * inside a declared ignore. Filing them under *still* would tell a reviewer the\n * edit missed the component it was written for, when in fact it hit it and a\n * rule forgave it. And a subject with no baseline was never asked the question at\n * all. Three outcomes, three lists, because a single count of *did not move*\n * would be three findings averaged into one wrong one.\n */\n\nimport type { BuildDetail } from '../review-types.js';\n\n/** One changed file's prediction, and what became of it. */\nexport interface Foreseen {\n /** Subjects the diff reaches through the components declared in this file. */\n readonly reached: number;\n /** Of those, the ones the run found changed. */\n readonly moved: number;\n /** Reached, compared, and nothing moved. The prediction that failed. */\n readonly still: readonly string[];\n /** Reached and moved, with every differing pixel inside a rule that forgives it. */\n readonly excluded: readonly string[];\n /** Reached and never compared — no baseline, or nothing to compare it against. */\n readonly uncompared: readonly string[];\n}\n\n/**\n * The prediction this file made, or `null` when the run made none.\n *\n * `null` covers both a build with no diff and a diff the run could not attribute\n * to subjects. Zeroes would say the file reaches nothing, which is a finding, and\n * this module must not manufacture one out of a missing input.\n */\nexport function foreseenBy(build: BuildDetail, file: string): Foreseen | null {\n const per = build.reach?.subjects;\n if (per === undefined) return null;\n\n const mine = new Set(\n (build.reach?.components ?? [])\n .filter((each) => each.trail[0] === file)\n .map((each) => each.component),\n );\n if (mine.size === 0) return null;\n\n const verdicts = new Map(build.subjects.map((each) => [each.subject, each.verdict]));\n const still: string[] = [];\n const excluded: string[] = [];\n const uncompared: string[] = [];\n let reached = 0;\n let moved = 0;\n\n for (const [subject, how] of Object.entries(per)) {\n if (!how.reached || !(how.through ?? []).some((each) => mine.has(each))) continue;\n reached += 1;\n\n switch (verdicts.get(subject)) {\n case 'changed':\n moved += 1;\n break;\n case 'unchanged':\n still.push(subject);\n break;\n case 'ignored':\n excluded.push(subject);\n break;\n default:\n // `new`, `incomparable`, and a subject the reach names that this build\n // has no row for. None of them answered the question.\n uncompared.push(subject);\n }\n }\n\n return {\n reached,\n moved,\n still: still.sort(),\n excluded: excluded.sort(),\n uncompared: uncompared.sort(),\n };\n}\n\nexport function unforeseen(build: BuildDetail): readonly string[] | null {\n const per = build.reach?.subjects;\n if (per === undefined) return null;\n\n return build.subjects\n .filter((each) => each.verdict === 'changed' && per[each.subject]?.reached !== true)\n .map((each) => each.subject)\n .sort();\n}\n"]}