@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,97 @@
1
+ /**
2
+ * The review surface, as ordinary React.
3
+ *
4
+ * No framework imports, no router, no data-fetching library, no CSS toolchain —
5
+ * so it mounts inside whatever the operator already deploys. `./next` is one
6
+ * such wiring, for a [vinext](https://vinext.io) app; a Vite SPA needs nothing
7
+ * from this package but these components and `createReviewClient`.
8
+ *
9
+ * ## What this surface is *for*, which is not "looking at two screenshots"
10
+ *
11
+ * Every hosted product in this category shows you a before and an after and asks
12
+ * you to spot the difference. That is the review blindness the whole project
13
+ * exists to refuse: the hundredth screenshot in a run gets the same glance as the
14
+ * first, and the three-hundred-and-first is approved without being read.
15
+ *
16
+ * So the ordering here is deliberate and is the opposite of the category's:
17
+ *
18
+ * 1. **The docket first**, and it is where a build opens — components named as
19
+ * *causes*, banded by what it costs to be wrong about them. One token change
20
+ * across 300 subjects is one item with a count.
21
+ * 2. **The regions on the image.** A reviewer sees which box moved and which
22
+ * component owns it, drawn over the render, cause and collateral distinct.
23
+ * 3. **The image last**, and only then as a comparison.
24
+ *
25
+ * Ranked by area a report of this kind is *wrong* — a container that was never
26
+ * edited and only reflowed outranks the edit, measured at 6× on one change. The
27
+ * ordering below comes from the tier that has provenance, which is why `cause`
28
+ * is a field on a region rather than a guess made here.
29
+ *
30
+ * ## Seven addresses, because this is a service and not a report
31
+ *
32
+ * This file is the router and nothing else. Every page is its own module and its
33
+ * own URL: [`builds.tsx`](./builds.tsx), [`docket.tsx`](./docket.tsx) for a build
34
+ * and the three stages inside it, [`change.tsx`](./change.tsx),
35
+ * [`subject.tsx`](./subject.tsx), [`run.tsx`](./run.tsx),
36
+ * [`history.tsx`](./history.tsx).
37
+ *
38
+ * The version this replaced held all of that in one component and three pieces of
39
+ * `useState`, which made every reading of a build the same address. A reviewer
40
+ * could not link a colleague to a change, could not open two in tabs, could not
41
+ * come back to one, and could not use the back button — and because there was one
42
+ * page, everything the run had to say was stacked on it. The build page reached
43
+ * twenty-seven thousand pixels, the divergence panel sat below four thousand of
44
+ * them, and the surface's best finding was in the part nobody scrolled to.
45
+ * [`route.ts`](./route.ts) is the address table both this and the Node service
46
+ * read, so a link that renders is a link that serves.
47
+ *
48
+ * ## And then the record, because "is this normal?" is the real question
49
+ *
50
+ * A difference is not a decision. The same 2px shift is a bug in a component
51
+ * nobody has touched since March and a Tuesday in one that moves in nineteen runs
52
+ * out of twenty, and a before-and-after cannot tell those apart. So the surface
53
+ * reaches the same history the CLI writes — churn, reach, stability, and the
54
+ * changelog of what was approved before — one subject at a time, and beside the
55
+ * change it is about rather than in a panel of its own.
56
+ */
57
+ import type { ReactElement } from 'react';
58
+ import type { ReviewClient } from './client.js';
59
+ import type { Route } from './route.js';
60
+ export { RegionOverlay, RegionTable, Viewer, modesFor, type ViewerMode } from './viewer.js';
61
+ export { ChangelogEntries, ChangelogPage, ChurnLine, StabilityLine, SubjectHistory } from './history.js';
62
+ export { ReachPanel, crossReach, type Crossing } from './reach.js';
63
+ export { BuildList, CoverageLine } from './builds.js';
64
+ export { OriginsPanel } from './origins.js';
65
+ export { SubjectPanel } from './subject.js';
66
+ export { Variations } from './variations.js';
67
+ export { Prose } from './shell.js';
68
+ export { originsOf, type Appearance, type Origin, type Origins } from './grouping.js';
69
+ export { Look, frameOf, sightings, type Frame, type Sighting } from './look.js';
70
+ export interface ReviewAppProps {
71
+ readonly client: ReviewClient;
72
+ /** Recorded on every decision. There are no accounts here; this is a name. */
73
+ readonly reviewer: string;
74
+ /** How many builds to list. The server's own default applies when absent. */
75
+ readonly limit?: number;
76
+ /**
77
+ * The address, when the host owns one.
78
+ *
79
+ * A Next app renders this inside a router that already holds the URL, and two
80
+ * things writing to one history stack is a back button that goes sideways.
81
+ * Given both of these, the surface reads the host's route and calls the host's
82
+ * navigate; given neither, it uses `pushState` itself.
83
+ */
84
+ readonly route?: Route | undefined;
85
+ readonly onNavigate?: ((route: Route) => void) | undefined;
86
+ }
87
+ /**
88
+ * The whole surface, mounted at whatever address it finds itself at.
89
+ *
90
+ * Give it a client and the reviewer's name and it is the entire review
91
+ * experience: builds awaiting decision, changes banded by what it costs to be
92
+ * wrong about them, regions drawn, and the approve and reject that write back. It
93
+ * holds no credential of its own, which is why the name is a prop and not
94
+ * something it asks the server for.
95
+ */
96
+ export declare function ReviewApp({ client, reviewer, limit, route, onNavigate, }: ReviewAppProps): ReactElement;
97
+ //# sourceMappingURL=review.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { BuildsPage } from './builds.js';
3
+ import { BuildPage } from './docket.js';
4
+ import { ChangelogPage } from './history.js';
5
+ import { useRoute } from './navigation.js';
6
+ import { Nowhere } from './shell.js';
7
+ export { RegionOverlay, RegionTable, Viewer, modesFor } from './viewer.js';
8
+ export { ChangelogEntries, ChangelogPage, ChurnLine, StabilityLine, SubjectHistory } from './history.js';
9
+ export { ReachPanel, crossReach } from './reach.js';
10
+ export { BuildList, CoverageLine } from './builds.js';
11
+ export { OriginsPanel } from './origins.js';
12
+ export { SubjectPanel } from './subject.js';
13
+ export { Variations } from './variations.js';
14
+ export { Prose } from './shell.js';
15
+ export { originsOf } from './grouping.js';
16
+ export { Look, frameOf, sightings } from './look.js';
17
+ /**
18
+ * The whole surface, mounted at whatever address it finds itself at.
19
+ *
20
+ * Give it a client and the reviewer's name and it is the entire review
21
+ * experience: builds awaiting decision, changes banded by what it costs to be
22
+ * wrong about them, regions drawn, and the approve and reject that write back. It
23
+ * holds no credential of its own, which is why the name is a prop and not
24
+ * something it asks the server for.
25
+ */
26
+ export function ReviewApp({ client, reviewer, limit, route, onNavigate, }) {
27
+ const [here, go] = useRoute(route, onNavigate);
28
+ // An address nobody claims is a rotted link, and rendering the front page for
29
+ // it would tell the reader their link worked.
30
+ if (here === undefined)
31
+ return _jsx(Nowhere, { go: go });
32
+ if (here.page === 'builds')
33
+ return _jsx(BuildsPage, { client: client, limit: limit, go: go });
34
+ if (here.page === 'changelog') {
35
+ return (_jsx("div", { className: "va-app", children: _jsx(ChangelogPage, { client: client, onBack: () => go({ page: 'builds' }) }) }));
36
+ }
37
+ return _jsx(BuildPage, { client: client, reviewer: reviewer, route: here, go: go });
38
+ }
39
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/ui/review.tsx"],"names":[],"mappings":";AA2DA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE3C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAErC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAmB,MAAM,aAAa,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACzG,OAAO,EAAE,UAAU,EAAE,UAAU,EAAiB,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAA8C,MAAM,eAAe,CAAC;AACtF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAA6B,MAAM,WAAW,CAAC;AAoBhF;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CAAC,EACxB,MAAM,EACN,QAAQ,EACR,KAAK,EACL,KAAK,EACL,UAAU,GACK;IACf,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;IAE/C,8EAA8E;IAC9E,8CAA8C;IAC9C,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,KAAC,OAAO,IAAC,EAAE,EAAE,EAAE,GAAI,CAAC;IAEnD,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAC,UAAU,IAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,GAAI,CAAC;IAExF,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC9B,OAAO,CACL,cAAK,SAAS,EAAC,QAAQ,YACrB,KAAC,aAAa,IAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,GAAI,GACnE,CACP,CAAC;IACJ,CAAC;IAED,OAAO,KAAC,SAAS,IAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,GAAI,CAAC;AAChF,CAAC","sourcesContent":["/**\n * The review surface, as ordinary React.\n *\n * No framework imports, no router, no data-fetching library, no CSS toolchain —\n * so it mounts inside whatever the operator already deploys. `./next` is one\n * such wiring, for a [vinext](https://vinext.io) app; a Vite SPA needs nothing\n * from this package but these components and `createReviewClient`.\n *\n * ## What this surface is *for*, which is not \"looking at two screenshots\"\n *\n * Every hosted product in this category shows you a before and an after and asks\n * you to spot the difference. That is the review blindness the whole project\n * exists to refuse: the hundredth screenshot in a run gets the same glance as the\n * first, and the three-hundred-and-first is approved without being read.\n *\n * So the ordering here is deliberate and is the opposite of the category's:\n *\n * 1. **The docket first**, and it is where a build opens — components named as\n * *causes*, banded by what it costs to be wrong about them. One token change\n * across 300 subjects is one item with a count.\n * 2. **The regions on the image.** A reviewer sees which box moved and which\n * component owns it, drawn over the render, cause and collateral distinct.\n * 3. **The image last**, and only then as a comparison.\n *\n * Ranked by area a report of this kind is *wrong* — a container that was never\n * edited and only reflowed outranks the edit, measured at 6× on one change. The\n * ordering below comes from the tier that has provenance, which is why `cause`\n * is a field on a region rather than a guess made here.\n *\n * ## Seven addresses, because this is a service and not a report\n *\n * This file is the router and nothing else. Every page is its own module and its\n * own URL: [`builds.tsx`](./builds.tsx), [`docket.tsx`](./docket.tsx) for a build\n * and the three stages inside it, [`change.tsx`](./change.tsx),\n * [`subject.tsx`](./subject.tsx), [`run.tsx`](./run.tsx),\n * [`history.tsx`](./history.tsx).\n *\n * The version this replaced held all of that in one component and three pieces of\n * `useState`, which made every reading of a build the same address. A reviewer\n * could not link a colleague to a change, could not open two in tabs, could not\n * come back to one, and could not use the back button — and because there was one\n * page, everything the run had to say was stacked on it. The build page reached\n * twenty-seven thousand pixels, the divergence panel sat below four thousand of\n * them, and the surface's best finding was in the part nobody scrolled to.\n * [`route.ts`](./route.ts) is the address table both this and the Node service\n * read, so a link that renders is a link that serves.\n *\n * ## And then the record, because \"is this normal?\" is the real question\n *\n * A difference is not a decision. The same 2px shift is a bug in a component\n * nobody has touched since March and a Tuesday in one that moves in nineteen runs\n * out of twenty, and a before-and-after cannot tell those apart. So the surface\n * reaches the same history the CLI writes — churn, reach, stability, and the\n * changelog of what was approved before — one subject at a time, and beside the\n * change it is about rather than in a panel of its own.\n */\n\nimport type { ReactElement } from 'react';\nimport type { ReviewClient } from './client.js';\nimport { BuildsPage } from './builds.js';\nimport { BuildPage } from './docket.js';\nimport { ChangelogPage } from './history.js';\nimport { useRoute } from './navigation.js';\nimport type { Route } from './route.js';\nimport { Nowhere } from './shell.js';\n\nexport { RegionOverlay, RegionTable, Viewer, modesFor, type ViewerMode } from './viewer.js';\nexport { ChangelogEntries, ChangelogPage, ChurnLine, StabilityLine, SubjectHistory } from './history.js';\nexport { ReachPanel, crossReach, type Crossing } from './reach.js';\nexport { BuildList, CoverageLine } from './builds.js';\nexport { OriginsPanel } from './origins.js';\nexport { SubjectPanel } from './subject.js';\nexport { Variations } from './variations.js';\nexport { Prose } from './shell.js';\nexport { originsOf, type Appearance, type Origin, type Origins } from './grouping.js';\nexport { Look, frameOf, sightings, type Frame, type Sighting } from './look.js';\n\nexport interface ReviewAppProps {\n readonly client: ReviewClient;\n /** Recorded on every decision. There are no accounts here; this is a name. */\n readonly reviewer: string;\n /** How many builds to list. The server's own default applies when absent. */\n readonly limit?: number;\n /**\n * The address, when the host owns one.\n *\n * A Next app renders this inside a router that already holds the URL, and two\n * things writing to one history stack is a back button that goes sideways.\n * Given both of these, the surface reads the host's route and calls the host's\n * navigate; given neither, it uses `pushState` itself.\n */\n readonly route?: Route | undefined;\n readonly onNavigate?: ((route: Route) => void) | undefined;\n}\n\n/**\n * The whole surface, mounted at whatever address it finds itself at.\n *\n * Give it a client and the reviewer's name and it is the entire review\n * experience: builds awaiting decision, changes banded by what it costs to be\n * wrong about them, regions drawn, and the approve and reject that write back. It\n * holds no credential of its own, which is why the name is a prop and not\n * something it asks the server for.\n */\nexport function ReviewApp({\n client,\n reviewer,\n limit,\n route,\n onNavigate,\n}: ReviewAppProps): ReactElement {\n const [here, go] = useRoute(route, onNavigate);\n\n // An address nobody claims is a rotted link, and rendering the front page for\n // it would tell the reader their link worked.\n if (here === undefined) return <Nowhere go={go} />;\n\n if (here.page === 'builds') return <BuildsPage client={client} limit={limit} go={go} />;\n\n if (here.page === 'changelog') {\n return (\n <div className=\"va-app\">\n <ChangelogPage client={client} onBack={() => go({ page: 'builds' })} />\n </div>\n );\n }\n\n return <BuildPage client={client} reviewer={reviewer} route={here} go={go} />;\n}\n"]}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * How a cause is drawn — as a heading over the changes it made, and on each of them.
3
+ *
4
+ * One module for both, because they are one claim printed twice and the two
5
+ * places must not be able to disagree. The rail says *`ui/button.tsx` — three
6
+ * renders*; the change page says *`Button` moved because `ui/button.tsx` is in
7
+ * this commit*, and a reviewer who clicked from one to the other and found two
8
+ * different files would stop believing either.
9
+ *
10
+ * ## The path is drawn, not summarised
11
+ *
12
+ * A basename is what a reviewer recognises and a path is what they can act on, so
13
+ * both are printed and the directory is dimmed rather than dropped. Two files
14
+ * called `button.tsx` in one build is an ordinary repository, and a heading that
15
+ * showed only the leaf would put them under one name.
16
+ *
17
+ * ## Siblings are the reason this is on the change page at all
18
+ *
19
+ * `Because` already names the rung — the file, or the parent, one hop up. What it
20
+ * cannot say is what *else* that file did, and that is the question a reviewer
21
+ * has after deciding one of them: `ProductCard.tsx` moved `CardFooter` here and
22
+ * `ProductCard` two rows down, and those are one edit with two consequences. The
23
+ * links are the rest of the edit.
24
+ */
25
+ import type { ReactElement } from 'react';
26
+ import type { BuildDetail } from '../review-types.js';
27
+ import { type Origin } from './grouping.js';
28
+ import { type Group } from './order.js';
29
+ import type { Route } from './route.js';
30
+ /**
31
+ * A band's heading: a path, a property, or a claim about a rung.
32
+ *
33
+ * The count beside a root is **renders**, not changes. *What did editing this
34
+ * file do* is answered in renders — one change over eleven of them is the answer
35
+ * a reviewer wants before they open it — while a band with no root counts changes,
36
+ * because a finding the commit does not account for had no blast radius to have.
37
+ */
38
+ export declare function RootHeading({ group, detail, go, }: {
39
+ readonly group: Group;
40
+ readonly detail: BuildDetail;
41
+ readonly go: (route: Route) => void;
42
+ }): ReactElement;
43
+ /**
44
+ * What in the commit this change hangs from, and what else hangs from it.
45
+ *
46
+ * `null` when nothing does. That is the docket's alarm bands — nothing reaches
47
+ * it, nothing explains it, no diff was read — and a line reading *from* with a
48
+ * blank after it would be the page implying a cause it does not have.
49
+ *
50
+ * Read back out of the same arrangement the rail draws, so the file named here is
51
+ * the heading the reviewer clicked to arrive.
52
+ */
53
+ export declare function causesOf(build: BuildDetail, origin: Origin): readonly Group[];
54
+ /**
55
+ * Whether one of these causes is already the file the component is declared in.
56
+ *
57
+ * The `edited` rung's whole shape: the file the commit changed and the file the
58
+ * component lives in are one path, and a head printing both says it twice while
59
+ * dropping the line number off the copy a reviewer can open. So the cause line
60
+ * carries the declaration and the declaration line stands down — asked here
61
+ * rather than decided twice, so the two lines cannot both appear or both vanish.
62
+ */
63
+ export declare function namesDeclaration(bands: readonly Group[], declared: string | undefined): boolean;
64
+ export declare function UnderRoot({ bands, component, declared, build, go, }: {
65
+ readonly bands: readonly Group[];
66
+ readonly component: string;
67
+ /** Where the component is declared, when the docket knows — usually with a line. */
68
+ readonly declared?: string | undefined;
69
+ readonly build: string;
70
+ readonly go: (route: Route) => void;
71
+ }): ReactElement | null;
72
+ //# sourceMappingURL=root.d.ts.map
@@ -0,0 +1,102 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { foreseenBy } from './foreseen.js';
3
+ import { originsOf } from './grouping.js';
4
+ import { docketOf } from './order.js';
5
+ import { Go } from './shell.js';
6
+ import { count, number } from './text.js';
7
+ /**
8
+ * A band's heading: a path, a property, or a claim about a rung.
9
+ *
10
+ * The count beside a root is **renders**, not changes. *What did editing this
11
+ * file do* is answered in renders — one change over eleven of them is the answer
12
+ * a reviewer wants before they open it — while a band with no root counts changes,
13
+ * because a finding the commit does not account for had no blast radius to have.
14
+ */
15
+ export function RootHeading({ group, detail, go, }) {
16
+ if (group.root === undefined) {
17
+ return (_jsxs("h3", { children: [group.title, " ", _jsx("span", { className: "va-num", children: number(group.changes.length) })] }));
18
+ }
19
+ return (_jsxs(_Fragment, { children: [_jsxs("h3", { className: `va-root va-root-${group.root.kind}`, title: group.title, children: [_jsx(Path, { root: group.root }), _jsx("span", { className: "va-num", children: number(group.root.renders) })] }), group.root.kind !== 'file' ? null : (_jsx(Prediction, { detail: detail, file: group.root.name, go: go }))] }));
20
+ }
21
+ /**
22
+ * What editing this file was going to do, beside what it did.
23
+ *
24
+ * Drawn only for a file, because only a file makes the prediction: the reach walk
25
+ * starts at a path the diff named. A property or an owner the commit never
26
+ * touched has no set of subjects it was expected to arrive at, and a line under
27
+ * one would be a number invented to fill the same slot.
28
+ */
29
+ function Prediction({ detail, file, go, }) {
30
+ const seen = foreseenBy(detail, file);
31
+ if (seen === null || seen.reached === 0)
32
+ return null;
33
+ const gaps = [
34
+ ['still', seen.still],
35
+ ['excluded', seen.excluded],
36
+ ['never compared', seen.uncompared],
37
+ ].filter(([, subjects]) => subjects.length > 0);
38
+ return (_jsxs("p", { className: "va-foreseen va-note", children: [_jsx("span", { className: "va-num", children: number(seen.reached) }), " reached", gaps.length === 0 ? _jsx(_Fragment, { children: " \u00B7 all moved" }) : null, gaps.map(([what, subjects]) => (_jsxs("span", { children: [' · ', _jsx("span", { className: "va-num", children: number(subjects.length) }), " ", what, ":", ' ', _jsx(Named, { subjects: subjects, build: detail.build, go: go })] }, what)))] }));
39
+ }
40
+ /** The subjects themselves, up to three, and a count of any it did not print. */
41
+ function Named({ subjects, build, go, }) {
42
+ const shown = subjects.slice(0, 3);
43
+ return (_jsxs(_Fragment, { children: [shown.map((subject, index) => (_jsxs("span", { children: [index === 0 ? '' : ', ', _jsx(Go, { to: { page: 'subject', build, subject }, go: go, className: "va-from-go", children: subject })] }, subject))), subjects.length > shown.length ? ` and ${number(subjects.length - shown.length)} more` : ''] }));
44
+ }
45
+ /** The name, with a file's directory dimmed and its basename left alone. */
46
+ function Path({ root, at, }) {
47
+ const name = at ?? root.name;
48
+ const cut = root.kind === 'file' ? name.lastIndexOf('/') + 1 : 0;
49
+ return (_jsxs(_Fragment, { children: [cut === 0 ? null : _jsx("span", { className: "va-root-in", children: name.slice(0, cut) }), _jsx("span", { className: "va-root-name", children: name.slice(cut) })] }));
50
+ }
51
+ /**
52
+ * The declared location when it is the root file with something after it.
53
+ *
54
+ * `startsWith` and not equality, because the declaration carries a line and the
55
+ * root does not. Guarded on the boundary so `button.tsx` does not swallow
56
+ * `button.test.tsx`, which is one character of prefix away and a different file.
57
+ */
58
+ function extending(declared, root) {
59
+ if (declared === undefined || !declared.startsWith(root))
60
+ return undefined;
61
+ const after = declared.slice(root.length);
62
+ return after === '' || after.startsWith(':') ? declared : undefined;
63
+ }
64
+ /**
65
+ * What in the commit this change hangs from, and what else hangs from it.
66
+ *
67
+ * `null` when nothing does. That is the docket's alarm bands — nothing reaches
68
+ * it, nothing explains it, no diff was read — and a line reading *from* with a
69
+ * blank after it would be the page implying a cause it does not have.
70
+ *
71
+ * Read back out of the same arrangement the rail draws, so the file named here is
72
+ * the heading the reviewer clicked to arrive.
73
+ */
74
+ export function causesOf(build, origin) {
75
+ return docketOf(build, originsOf(build).origins, 'story').filter((group) => group.root !== undefined &&
76
+ group.changes.some(({ origin: each }) => each.component === origin.component));
77
+ }
78
+ /**
79
+ * Whether one of these causes is already the file the component is declared in.
80
+ *
81
+ * The `edited` rung's whole shape: the file the commit changed and the file the
82
+ * component lives in are one path, and a head printing both says it twice while
83
+ * dropping the line number off the copy a reviewer can open. So the cause line
84
+ * carries the declaration and the declaration line stands down — asked here
85
+ * rather than decided twice, so the two lines cannot both appear or both vanish.
86
+ */
87
+ export function namesDeclaration(bands, declared) {
88
+ return bands.some((group) => group.root !== undefined && extending(declared, group.root.name) !== undefined);
89
+ }
90
+ export function UnderRoot({ bands, component, declared, build, go, }) {
91
+ if (bands.length === 0)
92
+ return null;
93
+ return (_jsx(_Fragment, { children: bands.map((group) => (_jsxs("p", { className: "va-from", children: [_jsx("span", { className: "va-from-mark", children: "from" }), ' ', _jsx("span", { className: `va-root va-root-${group.root?.kind ?? 'file'}`, title: group.title, children: group.root === undefined ? (group.title) : (_jsx(Path, { root: group.root, at: extending(declared, group.root.name) })) }), _jsx(Siblings, { group: group, component: component, build: build, go: go })] }, group.title))) }));
94
+ }
95
+ /** The other changes the same cause made, which is the rest of the reviewer's edit. */
96
+ function Siblings({ group, component, build, go, }) {
97
+ const others = group.changes.filter(({ origin }) => origin.component !== component);
98
+ if (others.length === 0)
99
+ return null;
100
+ return (_jsxs("span", { className: "va-note", children: [' ', "\u2014 which also moved", ' ', others.map(({ origin, of }, index) => (_jsxs("span", { children: [index === 0 ? '' : ', ', _jsx(Go, { to: { page: 'change', build, change: origin.component }, go: go, className: "va-from-go", children: origin.component }), ' ', _jsx("span", { className: "va-num", children: count(of ?? origin.appearances.length, 'render') })] }, origin.component)))] }));
101
+ }
102
+ //# sourceMappingURL=root.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root.js","sourceRoot":"","sources":["../../src/ui/root.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * How a cause is drawn — as a heading over the changes it made, and on each of them.\n *\n * One module for both, because they are one claim printed twice and the two\n * places must not be able to disagree. The rail says *`ui/button.tsx` — three\n * renders*; the change page says *`Button` moved because `ui/button.tsx` is in\n * this commit*, and a reviewer who clicked from one to the other and found two\n * different files would stop believing either.\n *\n * ## The path is drawn, not summarised\n *\n * A basename is what a reviewer recognises and a path is what they can act on, so\n * both are printed and the directory is dimmed rather than dropped. Two files\n * called `button.tsx` in one build is an ordinary repository, and a heading that\n * showed only the leaf would put them under one name.\n *\n * ## Siblings are the reason this is on the change page at all\n *\n * `Because` already names the rung — the file, or the parent, one hop up. What it\n * cannot say is what *else* that file did, and that is the question a reviewer\n * has after deciding one of them: `ProductCard.tsx` moved `CardFooter` here and\n * `ProductCard` two rows down, and those are one edit with two consequences. The\n * links are the rest of the edit.\n */\n\nimport type { ReactElement } from 'react';\nimport type { BuildDetail } from '../review-types.js';\nimport type { Root } from './cause.js';\nimport { foreseenBy } from './foreseen.js';\nimport { originsOf, type Origin } from './grouping.js';\nimport { docketOf, type Group } from './order.js';\nimport type { Route } from './route.js';\nimport { Go } from './shell.js';\nimport { count, number } from './text.js';\n\n/**\n * A band's heading: a path, a property, or a claim about a rung.\n *\n * The count beside a root is **renders**, not changes. *What did editing this\n * file do* is answered in renders — one change over eleven of them is the answer\n * a reviewer wants before they open it — while a band with no root counts changes,\n * because a finding the commit does not account for had no blast radius to have.\n */\nexport function RootHeading({\n group,\n detail,\n go,\n}: {\n readonly group: Group;\n readonly detail: BuildDetail;\n readonly go: (route: Route) => void;\n}): ReactElement {\n if (group.root === undefined) {\n return (\n <h3>\n {group.title} <span className=\"va-num\">{number(group.changes.length)}</span>\n </h3>\n );\n }\n\n return (\n <>\n <h3 className={`va-root va-root-${group.root.kind}`} title={group.title}>\n <Path root={group.root} />\n <span className=\"va-num\">{number(group.root.renders)}</span>\n </h3>\n {group.root.kind !== 'file' ? null : (\n <Prediction detail={detail} file={group.root.name} go={go} />\n )}\n </>\n );\n}\n\n/**\n * What editing this file was going to do, beside what it did.\n *\n * Drawn only for a file, because only a file makes the prediction: the reach walk\n * starts at a path the diff named. A property or an owner the commit never\n * touched has no set of subjects it was expected to arrive at, and a line under\n * one would be a number invented to fill the same slot.\n */\nfunction Prediction({\n detail,\n file,\n go,\n}: {\n readonly detail: BuildDetail;\n readonly file: string;\n readonly go: (route: Route) => void;\n}): ReactElement | null {\n const seen = foreseenBy(detail, file);\n if (seen === null || seen.reached === 0) return null;\n\n const gaps = (\n [\n ['still', seen.still],\n ['excluded', seen.excluded],\n ['never compared', seen.uncompared],\n ] as const\n ).filter(([, subjects]) => subjects.length > 0);\n\n return (\n <p className=\"va-foreseen va-note\">\n <span className=\"va-num\">{number(seen.reached)}</span> reached\n {gaps.length === 0 ? <> · all moved</> : null}\n {gaps.map(([what, subjects]) => (\n <span key={what}>\n {' · '}\n <span className=\"va-num\">{number(subjects.length)}</span> {what}:{' '}\n <Named subjects={subjects} build={detail.build} go={go} />\n </span>\n ))}\n </p>\n );\n}\n\n/** The subjects themselves, up to three, and a count of any it did not print. */\nfunction Named({\n subjects,\n build,\n go,\n}: {\n readonly subjects: readonly string[];\n readonly build: string;\n readonly go: (route: Route) => void;\n}): ReactElement {\n const shown = subjects.slice(0, 3);\n\n return (\n <>\n {shown.map((subject, index) => (\n <span key={subject}>\n {index === 0 ? '' : ', '}\n <Go to={{ page: 'subject', build, subject }} go={go} className=\"va-from-go\">\n {subject}\n </Go>\n </span>\n ))}\n {subjects.length > shown.length ? ` and ${number(subjects.length - shown.length)} more` : ''}\n </>\n );\n}\n\n/** The name, with a file's directory dimmed and its basename left alone. */\nfunction Path({\n root,\n at,\n}: {\n readonly root: Root;\n readonly at?: string | undefined;\n}): ReactElement {\n const name = at ?? root.name;\n const cut = root.kind === 'file' ? name.lastIndexOf('/') + 1 : 0;\n\n return (\n <>\n {cut === 0 ? null : <span className=\"va-root-in\">{name.slice(0, cut)}</span>}\n <span className=\"va-root-name\">{name.slice(cut)}</span>\n </>\n );\n}\n\n/**\n * The declared location when it is the root file with something after it.\n *\n * `startsWith` and not equality, because the declaration carries a line and the\n * root does not. Guarded on the boundary so `button.tsx` does not swallow\n * `button.test.tsx`, which is one character of prefix away and a different file.\n */\nfunction extending(declared: string | undefined, root: string): string | undefined {\n if (declared === undefined || !declared.startsWith(root)) return undefined;\n const after = declared.slice(root.length);\n return after === '' || after.startsWith(':') ? declared : undefined;\n}\n\n/**\n * What in the commit this change hangs from, and what else hangs from it.\n *\n * `null` when nothing does. That is the docket's alarm bands — nothing reaches\n * it, nothing explains it, no diff was read — and a line reading *from* with a\n * blank after it would be the page implying a cause it does not have.\n *\n * Read back out of the same arrangement the rail draws, so the file named here is\n * the heading the reviewer clicked to arrive.\n */\nexport function causesOf(build: BuildDetail, origin: Origin): readonly Group[] {\n return docketOf(build, originsOf(build).origins, 'story').filter(\n (group) =>\n group.root !== undefined &&\n group.changes.some(({ origin: each }) => each.component === origin.component),\n );\n}\n\n/**\n * Whether one of these causes is already the file the component is declared in.\n *\n * The `edited` rung's whole shape: the file the commit changed and the file the\n * component lives in are one path, and a head printing both says it twice while\n * dropping the line number off the copy a reviewer can open. So the cause line\n * carries the declaration and the declaration line stands down — asked here\n * rather than decided twice, so the two lines cannot both appear or both vanish.\n */\nexport function namesDeclaration(\n bands: readonly Group[],\n declared: string | undefined,\n): boolean {\n return bands.some(\n (group) => group.root !== undefined && extending(declared, group.root.name) !== undefined,\n );\n}\n\nexport function UnderRoot({\n bands,\n component,\n declared,\n build,\n go,\n}: {\n readonly bands: readonly Group[];\n readonly component: string;\n /** Where the component is declared, when the docket knows — usually with a line. */\n readonly declared?: string | undefined;\n readonly build: string;\n readonly go: (route: Route) => void;\n}): ReactElement | null {\n if (bands.length === 0) return null;\n\n return (\n <>\n {bands.map((group) => (\n <p key={group.title} className=\"va-from\">\n <span className=\"va-from-mark\">from</span>{' '}\n <span className={`va-root va-root-${group.root?.kind ?? 'file'}`} title={group.title}>\n {group.root === undefined ? (\n group.title\n ) : (\n <Path root={group.root} at={extending(declared, group.root.name)} />\n )}\n </span>\n <Siblings group={group} component={component} build={build} go={go} />\n </p>\n ))}\n </>\n );\n}\n\n/** The other changes the same cause made, which is the rest of the reviewer's edit. */\nfunction Siblings({\n group,\n component,\n build,\n go,\n}: {\n readonly group: Group;\n readonly component: string;\n readonly build: string;\n readonly go: (route: Route) => void;\n}): ReactElement | null {\n const others = group.changes.filter(({ origin }) => origin.component !== component);\n if (others.length === 0) return null;\n\n return (\n <span className=\"va-note\">\n {' '}\n — which also moved{' '}\n {others.map(({ origin, of }, index) => (\n <span key={origin.component}>\n {index === 0 ? '' : ', '}\n <Go to={{ page: 'change', build, change: origin.component }} go={go} className=\"va-from-go\">\n {origin.component}\n </Go>{' '}\n <span className=\"va-num\">{count(of ?? origin.appearances.length, 'render')}</span>\n </span>\n ))}\n </span>\n );\n}\n"]}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Where you are, as an address — because this is a service and not a report.
3
+ *
4
+ * The surface began as a build report rendered into a page, and it kept the
5
+ * report's idea of navigation: three `useState` values, one document, and a
6
+ * scroll bar. That has three costs and they compound. A reviewer cannot send
7
+ * anybody the thing they are looking at. A reload drops them back on the build
8
+ * list. And, worst, nothing on the page can be *elsewhere* — every panel a build
9
+ * knows about has to be on the build's own page, so the crossing against the
10
+ * previous run ends up ten thousand pixels below the change it is about.
11
+ *
12
+ * An address fixes all three at once, and the third is the one that matters: once
13
+ * a change has a URL, the panels that describe *that change* can live on it.
14
+ *
15
+ * ## One table, read from both ends
16
+ *
17
+ * {@link PAGE_PATTERNS} is the whole route set. The browser reads it to turn a
18
+ * path into a {@link Route}; the Node service reads the same table to decide
19
+ * which paths get the document. Two lists would drift, and the failure is silent
20
+ * in the worst direction — a link the client can render and the server answers
21
+ * with a 404.
22
+ *
23
+ * ## Disjoint from the API by construction
24
+ *
25
+ * The service answers `/review/…`, `/baseline/…`, `/cache/…` and `/history/…`.
26
+ * Pages live at `/builds/…` and `/changelog…`, which share no first segment with
27
+ * any of them. `serve.test.ts` holds that apart rather than trusting the reading:
28
+ * a page pattern that swallowed an API route would take the surface down and the
29
+ * ingest with it.
30
+ *
31
+ * ## Identifiers are encoded, because subjects have slashes in them
32
+ *
33
+ * A subject is `route/cart@1280`. Written raw it is two path segments and the
34
+ * pattern stops matching, so every identifier goes through
35
+ * `encodeURIComponent` on the way out and comes back through
36
+ * `decodeURIComponent` — and `route.test.ts` asserts the round trip on the
37
+ * awkward ones rather than on `Button`.
38
+ */
39
+ /** How the docket is arranged. Part of the address, so an arrangement is shareable. */
40
+ export type Order = 'story' | 'name' | 'size' | 'places';
41
+ /** Which page, and what it is about. The client's entire navigation state. */
42
+ export type Route = {
43
+ readonly page: 'builds';
44
+ } | {
45
+ readonly page: 'build';
46
+ readonly build: string;
47
+ readonly order?: Order;
48
+ } | {
49
+ readonly page: 'change';
50
+ readonly build: string;
51
+ readonly change: string;
52
+ } | {
53
+ readonly page: 'subject';
54
+ readonly build: string;
55
+ readonly subject: string;
56
+ } | {
57
+ readonly page: 'run';
58
+ readonly build: string;
59
+ } | {
60
+ readonly page: 'changelog';
61
+ };
62
+ /** One row of the route table: a page, the paths it answers, and what they carry. */
63
+ export interface PagePattern {
64
+ readonly page: Route['page'];
65
+ readonly at: RegExp;
66
+ /** The fields the captures fill, in order. */
67
+ readonly parts: readonly ('build' | 'change' | 'subject')[];
68
+ }
69
+ /**
70
+ * Every path this surface answers.
71
+ *
72
+ * Most specific first, though none of these can shadow another: a capture is
73
+ * `[^/]+`, so `/builds/7/run` cannot be read as a build named `7/run`. The order
74
+ * is for a reader, and for the day somebody adds a pattern that *can*.
75
+ */
76
+ export declare const PAGE_PATTERNS: readonly PagePattern[];
77
+ /**
78
+ * A path and its query as a route, or `undefined` when this surface owns neither.
79
+ *
80
+ * `undefined` rather than a fallback to the build list. A path nobody claims is a
81
+ * link that has rotted or a deployment mounted somewhere this build does not know
82
+ * about, and quietly showing the front page tells a reviewer their link worked.
83
+ */
84
+ export declare function parseRoute(href: string): Route | undefined;
85
+ /** The address of a route, ready for an `href` or a `pushState`. */
86
+ export declare function hrefOf(route: Route): string;
87
+ /**
88
+ * Whether a path is one of this surface's pages.
89
+ *
90
+ * The service's question, and it is deliberately not `parseRoute(path) !==
91
+ * undefined`: a server that had to construct a route to decide whether to send a
92
+ * document would be parsing query strings it will never read.
93
+ */
94
+ export declare function isPagePath(path: string): boolean;
95
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Where you are, as an address — because this is a service and not a report.
3
+ *
4
+ * The surface began as a build report rendered into a page, and it kept the
5
+ * report's idea of navigation: three `useState` values, one document, and a
6
+ * scroll bar. That has three costs and they compound. A reviewer cannot send
7
+ * anybody the thing they are looking at. A reload drops them back on the build
8
+ * list. And, worst, nothing on the page can be *elsewhere* — every panel a build
9
+ * knows about has to be on the build's own page, so the crossing against the
10
+ * previous run ends up ten thousand pixels below the change it is about.
11
+ *
12
+ * An address fixes all three at once, and the third is the one that matters: once
13
+ * a change has a URL, the panels that describe *that change* can live on it.
14
+ *
15
+ * ## One table, read from both ends
16
+ *
17
+ * {@link PAGE_PATTERNS} is the whole route set. The browser reads it to turn a
18
+ * path into a {@link Route}; the Node service reads the same table to decide
19
+ * which paths get the document. Two lists would drift, and the failure is silent
20
+ * in the worst direction — a link the client can render and the server answers
21
+ * with a 404.
22
+ *
23
+ * ## Disjoint from the API by construction
24
+ *
25
+ * The service answers `/review/…`, `/baseline/…`, `/cache/…` and `/history/…`.
26
+ * Pages live at `/builds/…` and `/changelog…`, which share no first segment with
27
+ * any of them. `serve.test.ts` holds that apart rather than trusting the reading:
28
+ * a page pattern that swallowed an API route would take the surface down and the
29
+ * ingest with it.
30
+ *
31
+ * ## Identifiers are encoded, because subjects have slashes in them
32
+ *
33
+ * A subject is `route/cart@1280`. Written raw it is two path segments and the
34
+ * pattern stops matching, so every identifier goes through
35
+ * `encodeURIComponent` on the way out and comes back through
36
+ * `decodeURIComponent` — and `route.test.ts` asserts the round trip on the
37
+ * awkward ones rather than on `Button`.
38
+ */
39
+ /**
40
+ * Every path this surface answers.
41
+ *
42
+ * Most specific first, though none of these can shadow another: a capture is
43
+ * `[^/]+`, so `/builds/7/run` cannot be read as a build named `7/run`. The order
44
+ * is for a reader, and for the day somebody adds a pattern that *can*.
45
+ */
46
+ export const PAGE_PATTERNS = [
47
+ { page: 'builds', at: /^\/(?:index\.html)?$/, parts: [] },
48
+ { page: 'changelog', at: /^\/changelog$/, parts: [] },
49
+ { page: 'run', at: /^\/builds\/([^/]+)\/run$/, parts: ['build'] },
50
+ { page: 'change', at: /^\/builds\/([^/]+)\/changes\/([^/]+)$/, parts: ['build', 'change'] },
51
+ { page: 'subject', at: /^\/builds\/([^/]+)\/subjects\/([^/]+)$/, parts: ['build', 'subject'] },
52
+ { page: 'build', at: /^\/builds\/([^/]+)$/, parts: ['build'] },
53
+ ];
54
+ const ORDERS = ['story', 'name', 'size', 'places'];
55
+ /**
56
+ * A path and its query as a route, or `undefined` when this surface owns neither.
57
+ *
58
+ * `undefined` rather than a fallback to the build list. A path nobody claims is a
59
+ * link that has rotted or a deployment mounted somewhere this build does not know
60
+ * about, and quietly showing the front page tells a reviewer their link worked.
61
+ */
62
+ export function parseRoute(href) {
63
+ const url = new URL(href, 'http://route.invalid');
64
+ for (const pattern of PAGE_PATTERNS) {
65
+ const found = pattern.at.exec(url.pathname);
66
+ if (found === null)
67
+ continue;
68
+ const carried = {};
69
+ pattern.parts.forEach((part, index) => {
70
+ carried[part] = decodeURIComponent(found[index + 1] ?? '');
71
+ });
72
+ if (pattern.page !== 'build')
73
+ return { page: pattern.page, ...carried };
74
+ const asked = url.searchParams.get('order');
75
+ const order = ORDERS.find((each) => each === asked);
76
+ return {
77
+ page: 'build',
78
+ build: carried['build'] ?? '',
79
+ ...(order === undefined ? {} : { order }),
80
+ };
81
+ }
82
+ return undefined;
83
+ }
84
+ /** The address of a route, ready for an `href` or a `pushState`. */
85
+ export function hrefOf(route) {
86
+ const at = (part) => encodeURIComponent(part);
87
+ switch (route.page) {
88
+ case 'builds':
89
+ return '/';
90
+ case 'build':
91
+ return route.order === undefined || route.order === 'story'
92
+ ? `/builds/${at(route.build)}`
93
+ : `/builds/${at(route.build)}?order=${route.order}`;
94
+ case 'change':
95
+ return `/builds/${at(route.build)}/changes/${at(route.change)}`;
96
+ case 'subject':
97
+ return `/builds/${at(route.build)}/subjects/${at(route.subject)}`;
98
+ case 'run':
99
+ return `/builds/${at(route.build)}/run`;
100
+ case 'changelog':
101
+ return '/changelog';
102
+ }
103
+ }
104
+ /**
105
+ * Whether a path is one of this surface's pages.
106
+ *
107
+ * The service's question, and it is deliberately not `parseRoute(path) !==
108
+ * undefined`: a server that had to construct a route to decide whether to send a
109
+ * document would be parsing query strings it will never read.
110
+ */
111
+ export function isPagePath(path) {
112
+ return PAGE_PATTERNS.some((pattern) => pattern.at.test(path));
113
+ }
114
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../../src/ui/route.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where you are, as an address — because this is a service and not a report.\n *\n * The surface began as a build report rendered into a page, and it kept the\n * report's idea of navigation: three `useState` values, one document, and a\n * scroll bar. That has three costs and they compound. A reviewer cannot send\n * anybody the thing they are looking at. A reload drops them back on the build\n * list. And, worst, nothing on the page can be *elsewhere* — every panel a build\n * knows about has to be on the build's own page, so the crossing against the\n * previous run ends up ten thousand pixels below the change it is about.\n *\n * An address fixes all three at once, and the third is the one that matters: once\n * a change has a URL, the panels that describe *that change* can live on it.\n *\n * ## One table, read from both ends\n *\n * {@link PAGE_PATTERNS} is the whole route set. The browser reads it to turn a\n * path into a {@link Route}; the Node service reads the same table to decide\n * which paths get the document. Two lists would drift, and the failure is silent\n * in the worst direction — a link the client can render and the server answers\n * with a 404.\n *\n * ## Disjoint from the API by construction\n *\n * The service answers `/review/…`, `/baseline/…`, `/cache/…` and `/history/…`.\n * Pages live at `/builds/…` and `/changelog…`, which share no first segment with\n * any of them. `serve.test.ts` holds that apart rather than trusting the reading:\n * a page pattern that swallowed an API route would take the surface down and the\n * ingest with it.\n *\n * ## Identifiers are encoded, because subjects have slashes in them\n *\n * A subject is `route/cart@1280`. Written raw it is two path segments and the\n * pattern stops matching, so every identifier goes through\n * `encodeURIComponent` on the way out and comes back through\n * `decodeURIComponent` — and `route.test.ts` asserts the round trip on the\n * awkward ones rather than on `Button`.\n */\n\n/** How the docket is arranged. Part of the address, so an arrangement is shareable. */\nexport type Order = 'story' | 'name' | 'size' | 'places';\n\n/** Which page, and what it is about. The client's entire navigation state. */\nexport type Route =\n | { readonly page: 'builds' }\n | { readonly page: 'build'; readonly build: string; readonly order?: Order }\n | { readonly page: 'change'; readonly build: string; readonly change: string }\n | { readonly page: 'subject'; readonly build: string; readonly subject: string }\n | { readonly page: 'run'; readonly build: string }\n | { readonly page: 'changelog' };\n\n/** One row of the route table: a page, the paths it answers, and what they carry. */\nexport interface PagePattern {\n readonly page: Route['page'];\n readonly at: RegExp;\n /** The fields the captures fill, in order. */\n readonly parts: readonly ('build' | 'change' | 'subject')[];\n}\n\n/**\n * Every path this surface answers.\n *\n * Most specific first, though none of these can shadow another: a capture is\n * `[^/]+`, so `/builds/7/run` cannot be read as a build named `7/run`. The order\n * is for a reader, and for the day somebody adds a pattern that *can*.\n */\nexport const PAGE_PATTERNS: readonly PagePattern[] = [\n { page: 'builds', at: /^\\/(?:index\\.html)?$/, parts: [] },\n { page: 'changelog', at: /^\\/changelog$/, parts: [] },\n { page: 'run', at: /^\\/builds\\/([^/]+)\\/run$/, parts: ['build'] },\n { page: 'change', at: /^\\/builds\\/([^/]+)\\/changes\\/([^/]+)$/, parts: ['build', 'change'] },\n { page: 'subject', at: /^\\/builds\\/([^/]+)\\/subjects\\/([^/]+)$/, parts: ['build', 'subject'] },\n { page: 'build', at: /^\\/builds\\/([^/]+)$/, parts: ['build'] },\n];\n\nconst ORDERS: readonly Order[] = ['story', 'name', 'size', 'places'];\n\n/**\n * A path and its query as a route, or `undefined` when this surface owns neither.\n *\n * `undefined` rather than a fallback to the build list. A path nobody claims is a\n * link that has rotted or a deployment mounted somewhere this build does not know\n * about, and quietly showing the front page tells a reviewer their link worked.\n */\nexport function parseRoute(href: string): Route | undefined {\n const url = new URL(href, 'http://route.invalid');\n\n for (const pattern of PAGE_PATTERNS) {\n const found = pattern.at.exec(url.pathname);\n if (found === null) continue;\n\n const carried: Record<string, string> = {};\n pattern.parts.forEach((part, index) => {\n carried[part] = decodeURIComponent(found[index + 1] ?? '');\n });\n\n if (pattern.page !== 'build') return { page: pattern.page, ...carried } as Route;\n\n const asked = url.searchParams.get('order');\n const order = ORDERS.find((each) => each === asked);\n return {\n page: 'build',\n build: carried['build'] ?? '',\n ...(order === undefined ? {} : { order }),\n };\n }\n\n return undefined;\n}\n\n/** The address of a route, ready for an `href` or a `pushState`. */\nexport function hrefOf(route: Route): string {\n const at = (part: string): string => encodeURIComponent(part);\n\n switch (route.page) {\n case 'builds':\n return '/';\n case 'build':\n return route.order === undefined || route.order === 'story'\n ? `/builds/${at(route.build)}`\n : `/builds/${at(route.build)}?order=${route.order}`;\n case 'change':\n return `/builds/${at(route.build)}/changes/${at(route.change)}`;\n case 'subject':\n return `/builds/${at(route.build)}/subjects/${at(route.subject)}`;\n case 'run':\n return `/builds/${at(route.build)}/run`;\n case 'changelog':\n return '/changelog';\n }\n}\n\n/**\n * Whether a path is one of this surface's pages.\n *\n * The service's question, and it is deliberately not `parseRoute(path) !==\n * undefined`: a server that had to construct a route to decide whether to send a\n * document would be parsing query strings it will never read.\n */\nexport function isPagePath(path: string): boolean {\n return PAGE_PATTERNS.some((pattern) => pattern.at.test(path));\n}\n"]}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The run, in full — everything the report says that is not one change.
3
+ *
4
+ * This is the material that used to sit under the docket on the build page, and
5
+ * moving it is the whole of the layout argument. Reach, the crossing against the
6
+ * last run, the variation lattice, what a declaration settled, what was declared,
7
+ * what was never observed: every one of those is worth reading, and none of them
8
+ * is worth reading *while deciding whether to approve a restyle of `Button`*. On
9
+ * one page they were four thousand pixels of scroll between a reviewer and the
10
+ * second change in the build.
11
+ *
12
+ * So they have an address. A reviewer working a docket never opens it; a reviewer
13
+ * asking *what did this run actually do* opens nothing else. The crossing is
14
+ * handed down rather than fetched again — the build page has already asked, and
15
+ * the two readings of one comparison must not be able to disagree.
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
+ export declare function RunPage({ client, build, crossing, }: {
22
+ readonly client: ReviewClient;
23
+ readonly build: BuildDetail;
24
+ readonly crossing: Crossing;
25
+ }): ReactElement;
26
+ //# sourceMappingURL=run.d.ts.map