@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,101 @@
1
+ /**
2
+ * Where one change's renders stopped agreeing with each other.
3
+ *
4
+ * A change is approved once and lands many times. `Button` moved in seven
5
+ * renders and the page says so, which reads as one thing having happened seven
6
+ * times — and on this build it is two things. Two of those renders moved by
7
+ * 6,418 pixels into one shape and five moved by about 3,500 into another, so a
8
+ * reviewer pressing approve is signing off on two outcomes under one name and
9
+ * has been shown neither.
10
+ *
11
+ * The shape is already recorded: a region carries a fingerprint of the
12
+ * difference, so renders that moved *identically* share one and renders that
13
+ * absorbed the same edit differently do not. Grouping by it costs nothing and
14
+ * turns `7 renders · 2 shapes` — a count nobody can act on — into the two groups
15
+ * and what separates them.
16
+ *
17
+ * ## Not `partingOf`
18
+ *
19
+ * `@variance-authority/core`'s parting reads two documents and climbs to the
20
+ * hook cell that decided a difference. This never opens a document. It divides
21
+ * one component's renders by the digest of what happened to each, which is the
22
+ * same question — *where did these part* — asked at the only grain a review
23
+ * surface has, and answerable on data the build already carried.
24
+ *
25
+ * ## The shared name is a name, not a cause
26
+ *
27
+ * When every subject in a group extends one prefix and no other group's does,
28
+ * that prefix is printed. It is the inference `VariationRecord`'s `named`
29
+ * already makes — a subject whose name extends another's is that subject plus an
30
+ * axis — and it is correct exactly as often as the naming convention is kept. So
31
+ * it is printed as a label beside the group and never as a because: *these two
32
+ * are the `route/detail` renders*, not *`route/detail` caused this*.
33
+ *
34
+ * One shape is reported too. *Seven renders, one shape* is the answer that makes
35
+ * the divided case legible, and a section that appeared only on divergence would
36
+ * leave a reviewer unable to tell agreement from a panel that had not loaded.
37
+ */
38
+ export function partedBy(origin) {
39
+ const groups = new Map();
40
+ const unshaped = [];
41
+ for (const each of origin.appearances) {
42
+ if (each.shape === undefined) {
43
+ unshaped.push(each.subject.subject);
44
+ continue;
45
+ }
46
+ groups.set(each.shape, [...(groups.get(each.shape) ?? []), each]);
47
+ }
48
+ const found = [...groups].map(([shape, members]) => {
49
+ const pixels = members.map((each) => each.pixels);
50
+ return {
51
+ shape,
52
+ renders: members.map((each) => each.subject.subject).sort(),
53
+ least: Math.min(...pixels),
54
+ most: Math.max(...pixels),
55
+ };
56
+ });
57
+ const shapes = found
58
+ .map((group) => {
59
+ const only = nameOf(group.renders, found.filter((other) => other.shape !== group.shape).flatMap((other) => other.renders));
60
+ return only === undefined ? group : { ...group, only };
61
+ })
62
+ .sort((left, right) => right.most - left.most || left.shape.localeCompare(right.shape));
63
+ return { shapes, unshaped: unshaped.sort() };
64
+ }
65
+ /** Boundaries a subject name is built from, so a prefix is cut at a joint. */
66
+ const JOINTS = new Set(['/', '@', ':', '-', '.', '_']);
67
+ /**
68
+ * The name these renders share, when no other group's renders share it.
69
+ *
70
+ * `undefined` for a single render, where the prefix would be the subject itself
71
+ * and printing it beside the subject says nothing twice.
72
+ *
73
+ * The common prefix is only a name when it ends where the renders do — at a
74
+ * joint, or at the end of one of them. `route/detail@1280` and
75
+ * `route/detail@1024` have `route/detail@1` in common and no such viewport
76
+ * exists; the name is `route/detail`, which is then shared with the other group
77
+ * and so is not printed at all.
78
+ */
79
+ function nameOf(renders, others) {
80
+ if (renders.length < 2)
81
+ return undefined;
82
+ const [first = '', ...rest] = renders;
83
+ let cut = first.length;
84
+ for (const each of rest) {
85
+ let index = 0;
86
+ while (index < cut && index < each.length && each[index] === first[index])
87
+ index += 1;
88
+ cut = index;
89
+ }
90
+ if (!renders.every((each) => each.length === cut || JOINTS.has(each[cut] ?? ''))) {
91
+ while (cut > 0 && !JOINTS.has(first[cut - 1] ?? ''))
92
+ cut -= 1;
93
+ }
94
+ while (cut > 0 && JOINTS.has(first[cut - 1] ?? ''))
95
+ cut -= 1;
96
+ const shared = first.slice(0, cut);
97
+ if (shared === '')
98
+ return undefined;
99
+ return others.some((each) => each.startsWith(shared)) ? undefined : shared;
100
+ }
101
+ //# sourceMappingURL=parted.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parted.js","sourceRoot":"","sources":["../../src/ui/parted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAkCH,MAAM,UAAU,QAAQ,CAAC,MAAc;IACrC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC/C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACtC,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACpC,SAAS;QACX,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;IACpE,CAAC;IAED,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,EAAE;QACjD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClD,OAAO;YACL,KAAK;YACL,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE;YAC3D,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;YAC1B,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;SAC1B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,KAAK;SACjB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACb,MAAM,IAAI,GAAG,MAAM,CACjB,KAAK,CAAC,OAAO,EACb,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CACvF,CAAC;QACF,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;IACzD,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IAE1F,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;AAC/C,CAAC;AAED,8EAA8E;AAC9E,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAEvD;;;;;;;;;;;GAWG;AACH,SAAS,MAAM,CAAC,OAA0B,EAAE,MAAyB;IACnE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAEzC,MAAM,CAAC,KAAK,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC;IACtC,IAAI,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC;IACvB,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,OAAO,KAAK,GAAG,GAAG,IAAI,KAAK,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC;YAAE,KAAK,IAAI,CAAC,CAAC;QACtF,GAAG,GAAG,KAAK,CAAC;IACd,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;QACjF,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;YAAE,GAAG,IAAI,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,GAAG,GAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QAAE,GAAG,IAAI,CAAC,CAAC;IAC7D,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACnC,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IAEpC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;AAC7E,CAAC","sourcesContent":["/**\n * Where one change's renders stopped agreeing with each other.\n *\n * A change is approved once and lands many times. `Button` moved in seven\n * renders and the page says so, which reads as one thing having happened seven\n * times — and on this build it is two things. Two of those renders moved by\n * 6,418 pixels into one shape and five moved by about 3,500 into another, so a\n * reviewer pressing approve is signing off on two outcomes under one name and\n * has been shown neither.\n *\n * The shape is already recorded: a region carries a fingerprint of the\n * difference, so renders that moved *identically* share one and renders that\n * absorbed the same edit differently do not. Grouping by it costs nothing and\n * turns `7 renders · 2 shapes` — a count nobody can act on — into the two groups\n * and what separates them.\n *\n * ## Not `partingOf`\n *\n * `@variance-authority/core`'s parting reads two documents and climbs to the\n * hook cell that decided a difference. This never opens a document. It divides\n * one component's renders by the digest of what happened to each, which is the\n * same question — *where did these part* — asked at the only grain a review\n * surface has, and answerable on data the build already carried.\n *\n * ## The shared name is a name, not a cause\n *\n * When every subject in a group extends one prefix and no other group's does,\n * that prefix is printed. It is the inference `VariationRecord`'s `named`\n * already makes — a subject whose name extends another's is that subject plus an\n * axis — and it is correct exactly as often as the naming convention is kept. So\n * it is printed as a label beside the group and never as a because: *these two\n * are the `route/detail` renders*, not *`route/detail` caused this*.\n *\n * One shape is reported too. *Seven renders, one shape* is the answer that makes\n * the divided case legible, and a section that appeared only on divergence would\n * leave a reviewer unable to tell agreement from a panel that had not loaded.\n */\n\nimport type { Appearance, Origin } from './grouping.js';\n\n/** One group of renders that moved identically. */\nexport interface Shape {\n /** The fingerprint they share. Not printed; it is an identity, not a finding. */\n readonly shape: string;\n /** Subjects in it, sorted. */\n readonly renders: readonly string[];\n readonly least: number;\n readonly most: number;\n /**\n * The name every render here extends and no other group's does.\n *\n * Absent when the group has one render — the subject is already the coordinate\n * — or when the names have nothing in common that is theirs alone.\n */\n readonly only?: string;\n}\n\n/** One change's renders, divided by what happened to each. */\nexport interface Parted {\n readonly shapes: readonly Shape[];\n /**\n * Renders whose leading region recorded no fingerprint.\n *\n * Their own list, never a shape. Two renders that both recorded nothing have\n * not been found to agree, and folding them together would report the\n * identical difference twice about a pair nothing measured.\n */\n readonly unshaped: readonly string[];\n}\n\nexport function partedBy(origin: Origin): Parted {\n const groups = new Map<string, Appearance[]>();\n const unshaped: string[] = [];\n\n for (const each of origin.appearances) {\n if (each.shape === undefined) {\n unshaped.push(each.subject.subject);\n continue;\n }\n groups.set(each.shape, [...(groups.get(each.shape) ?? []), each]);\n }\n\n const found = [...groups].map(([shape, members]) => {\n const pixels = members.map((each) => each.pixels);\n return {\n shape,\n renders: members.map((each) => each.subject.subject).sort(),\n least: Math.min(...pixels),\n most: Math.max(...pixels),\n };\n });\n\n const shapes = found\n .map((group) => {\n const only = nameOf(\n group.renders,\n found.filter((other) => other.shape !== group.shape).flatMap((other) => other.renders),\n );\n return only === undefined ? group : { ...group, only };\n })\n .sort((left, right) => right.most - left.most || left.shape.localeCompare(right.shape));\n\n return { shapes, unshaped: unshaped.sort() };\n}\n\n/** Boundaries a subject name is built from, so a prefix is cut at a joint. */\nconst JOINTS = new Set(['/', '@', ':', '-', '.', '_']);\n\n/**\n * The name these renders share, when no other group's renders share it.\n *\n * `undefined` for a single render, where the prefix would be the subject itself\n * and printing it beside the subject says nothing twice.\n *\n * The common prefix is only a name when it ends where the renders do — at a\n * joint, or at the end of one of them. `route/detail@1280` and\n * `route/detail@1024` have `route/detail@1` in common and no such viewport\n * exists; the name is `route/detail`, which is then shared with the other group\n * and so is not printed at all.\n */\nfunction nameOf(renders: readonly string[], others: readonly string[]): string | undefined {\n if (renders.length < 2) return undefined;\n\n const [first = '', ...rest] = renders;\n let cut = first.length;\n for (const each of rest) {\n let index = 0;\n while (index < cut && index < each.length && each[index] === first[index]) index += 1;\n cut = index;\n }\n\n if (!renders.every((each) => each.length === cut || JOINTS.has(each[cut] ?? ''))) {\n while (cut > 0 && !JOINTS.has(first[cut - 1] ?? '')) cut -= 1;\n }\n while (cut > 0 && JOINTS.has(first[cut - 1] ?? '')) cut -= 1;\n const shared = first.slice(0, cut);\n if (shared === '') return undefined;\n\n return others.some((each) => each.startsWith(shared)) ? undefined : shared;\n}\n"]}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The rail: every subject worth a decision, grouped by verdict.
3
+ *
4
+ * Apart from [`review.tsx`](./review.tsx) because it is the one control the page
5
+ * is navigated by and it answers a question of its own — *which of these is worth
6
+ * opening* — before any render is fetched. The note under each name is the
7
+ * component the semantic tier blamed and the size of the difference, which is
8
+ * enough to decide without a round trip.
9
+ *
10
+ * What it does not carry is anything green. That is [`settled.tsx`](./settled.js)'s,
11
+ * and the split is the point: a rail that listed a subject an ignore had already
12
+ * decided would put it in a queue, under a header counting what awaits review.
13
+ */
14
+ import type { ReactElement } from 'react';
15
+ import type { SubjectView } from '../review-types.js';
16
+ /**
17
+ * Every subject worth a decision, grouped by verdict and readable at a glance.
18
+ *
19
+ * The rail exists so that choosing what to look at costs a glance rather than a
20
+ * scroll: the note under each name is the component the tier blamed and the size
21
+ * of the difference, which is enough to decide whether this one needs opening at
22
+ * all. Sorted largest-first inside each group, for the same reason the docket is.
23
+ */
24
+ export declare function SubjectRail({ subjects, causes, variations, selected, onSelect, }: {
25
+ readonly subjects: readonly SubjectView[];
26
+ readonly causes: number;
27
+ readonly variations: number;
28
+ readonly selected: string | null;
29
+ readonly onSelect: (subject: string | null) => void;
30
+ }): ReactElement;
31
+ //# sourceMappingURL=rail.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { causeOf } from './lead.js';
3
+ import { briefly, count } from './text.js';
4
+ /**
5
+ * Every subject worth a decision, grouped by verdict and readable at a glance.
6
+ *
7
+ * The rail exists so that choosing what to look at costs a glance rather than a
8
+ * scroll: the note under each name is the component the tier blamed and the size
9
+ * of the difference, which is enough to decide whether this one needs opening at
10
+ * all. Sorted largest-first inside each group, for the same reason the docket is.
11
+ */
12
+ export function SubjectRail({ subjects, causes, variations, selected, onSelect, }) {
13
+ const groups = [...new Set(subjects.map((subject) => subject.verdict))];
14
+ return (_jsxs("div", { className: "va-rail-list va-scroll", children: [_jsx("button", { type: "button", className: selected === null ? 'va-rail-item va-current' : 'va-rail-item', onClick: () => onSelect(null), children: _jsxs("span", { className: "va-rail-body", children: [_jsx("span", { className: "va-rail-name", children: "The docket" }), _jsxs("span", { className: "va-rail-note", children: [count(causes, 'cause'), " \u00B7 ", count(variations, 'variation')] })] }) }), groups.map((verdict) => {
15
+ const rows = subjects
16
+ .filter((each) => each.verdict === verdict)
17
+ .sort((left, right) => right.changedPixels - left.changedPixels);
18
+ return (_jsxs("section", { children: [_jsxs("h2", { className: "va-rail-group", children: [verdict, _jsx("span", { className: "va-rail-count va-num", children: rows.length })] }), _jsx("ul", { children: rows.map((subject) => (_jsx("li", { children: _jsxs("button", { type: "button", className: subject.subject === selected ? 'va-rail-item va-current' : 'va-rail-item', onClick: () => onSelect(subject.subject), children: [_jsx("span", { className: `va-dot va-${subject.verdict}` }), _jsxs("span", { className: "va-rail-body", children: [_jsx("span", { className: "va-rail-name", children: subject.subject }), _jsxs("span", { className: "va-rail-note", children: [causeOf(subject) ?? 'no component named', subject.changedPixels > 0 ? ` · ${briefly(subject)}` : ''] })] }), subject.decision === null ? null : (_jsx("span", { className: `va-mark va-${subject.decision.decision}`, children: subject.decision.decision === 'approved' ? '✓' : '✗' }))] }) }, subject.subject))) })] }, verdict));
19
+ })] }));
20
+ }
21
+ //# sourceMappingURL=rail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rail.js","sourceRoot":"","sources":["../../src/ui/rail.tsx"],"names":[],"mappings":";AAgBA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,QAAQ,EACR,MAAM,EACN,UAAU,EACV,QAAQ,EACR,QAAQ,GAOT;IACC,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAExE,OAAO,CACL,eAAK,SAAS,EAAC,wBAAwB,aACrC,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,cAAc,EACzE,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,YAE7B,gBAAM,SAAS,EAAC,cAAc,aAC5B,eAAM,SAAS,EAAC,cAAc,2BAAkB,EAChD,gBAAM,SAAS,EAAC,cAAc,aAC3B,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,cAAK,KAAK,CAAC,UAAU,EAAE,WAAW,CAAC,IACrD,IACF,GACA,EAER,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;gBACtB,MAAM,IAAI,GAAG,QAAQ;qBAClB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,KAAK,OAAO,CAAC;qBAC1C,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC;gBAEnE,OAAO,CACL,8BACE,cAAI,SAAS,EAAC,eAAe,aAC1B,OAAO,EACR,eAAM,SAAS,EAAC,sBAAsB,YAAE,IAAI,CAAC,MAAM,GAAQ,IACxD,EACL,uBACG,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CACrB,uBACE,kBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EACP,OAAO,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,cAAc,EAE3E,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,aAExC,eAAM,SAAS,EAAE,aAAa,OAAO,CAAC,OAAO,EAAE,GAAI,EACnD,gBAAM,SAAS,EAAC,cAAc,aAC5B,eAAM,SAAS,EAAC,cAAc,YAAE,OAAO,CAAC,OAAO,GAAQ,EACvD,gBAAM,SAAS,EAAC,cAAc,aAC3B,OAAO,CAAC,OAAO,CAAC,IAAI,oBAAoB,EACxC,OAAO,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IACrD,IACF,EACN,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAClC,eAAM,SAAS,EAAE,cAAc,OAAO,CAAC,QAAQ,CAAC,QAAQ,EAAE,YACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAChD,CACR,IACM,IArBF,OAAO,CAAC,OAAO,CAsBnB,CACN,CAAC,GACC,KA/BO,OAAO,CAgCX,CACX,CAAC;YACJ,CAAC,CAAC,IACE,CACP,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The rail: every subject worth a decision, grouped by verdict.\n *\n * Apart from [`review.tsx`](./review.tsx) because it is the one control the page\n * is navigated by and it answers a question of its own — *which of these is worth\n * opening* — before any render is fetched. The note under each name is the\n * component the semantic tier blamed and the size of the difference, which is\n * enough to decide without a round trip.\n *\n * What it does not carry is anything green. That is [`settled.tsx`](./settled.js)'s,\n * and the split is the point: a rail that listed a subject an ignore had already\n * decided would put it in a queue, under a header counting what awaits review.\n */\n\nimport type { ReactElement } from 'react';\nimport type { SubjectView } from '../review-types.js';\nimport { causeOf } from './lead.js';\nimport { briefly, count } from './text.js';\n\n/**\n * Every subject worth a decision, grouped by verdict and readable at a glance.\n *\n * The rail exists so that choosing what to look at costs a glance rather than a\n * scroll: the note under each name is the component the tier blamed and the size\n * of the difference, which is enough to decide whether this one needs opening at\n * all. Sorted largest-first inside each group, for the same reason the docket is.\n */\nexport function SubjectRail({\n subjects,\n causes,\n variations,\n selected,\n onSelect,\n}: {\n readonly subjects: readonly SubjectView[];\n readonly causes: number;\n readonly variations: number;\n readonly selected: string | null;\n readonly onSelect: (subject: string | null) => void;\n}): ReactElement {\n const groups = [...new Set(subjects.map((subject) => subject.verdict))];\n\n return (\n <div className=\"va-rail-list va-scroll\">\n <button\n type=\"button\"\n className={selected === null ? 'va-rail-item va-current' : 'va-rail-item'}\n onClick={() => onSelect(null)}\n >\n <span className=\"va-rail-body\">\n <span className=\"va-rail-name\">The docket</span>\n <span className=\"va-rail-note\">\n {count(causes, 'cause')} · {count(variations, 'variation')}\n </span>\n </span>\n </button>\n\n {groups.map((verdict) => {\n const rows = subjects\n .filter((each) => each.verdict === verdict)\n .sort((left, right) => right.changedPixels - left.changedPixels);\n\n return (\n <section key={verdict}>\n <h2 className=\"va-rail-group\">\n {verdict}\n <span className=\"va-rail-count va-num\">{rows.length}</span>\n </h2>\n <ul>\n {rows.map((subject) => (\n <li key={subject.subject}>\n <button\n type=\"button\"\n className={\n subject.subject === selected ? 'va-rail-item va-current' : 'va-rail-item'\n }\n onClick={() => onSelect(subject.subject)}\n >\n <span className={`va-dot va-${subject.verdict}`} />\n <span className=\"va-rail-body\">\n <span className=\"va-rail-name\">{subject.subject}</span>\n <span className=\"va-rail-note\">\n {causeOf(subject) ?? 'no component named'}\n {subject.changedPixels > 0 ? ` · ${briefly(subject)}` : ''}\n </span>\n </span>\n {subject.decision === null ? null : (\n <span className={`va-mark va-${subject.decision.decision}`}>\n {subject.decision.decision === 'approved' ? '✓' : '✗'}\n </span>\n )}\n </button>\n </li>\n ))}\n </ul>\n </section>\n );\n })}\n </div>\n );\n}\n"]}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * What the commit reaches, crossed against what the run saw.
3
+ *
4
+ * Every other panel on this page answers *what changed*. This one answers *what
5
+ * could have*, and the whole value is in the crossing: a verdict alone cannot say
6
+ * whether a green subject was supposed to be green, and a diff alone cannot say
7
+ * whether the file somebody edited paints anything at all.
8
+ *
9
+ * Four states come out of it, and two are findings no image comparison can reach:
10
+ *
11
+ * - **reached, and moved** — the expected case, and the trail names the file.
12
+ * - **reached, and still** — the commit reaches this subject and changed no pixel
13
+ * in it. Every tool in this category is silent about a green subject, so an
14
+ * author who believed they were changing this surface learns it here or not at
15
+ * all.
16
+ * - **moved, unreached** — nothing in the commit arrives here and it moved anyway.
17
+ * That is a flake, an input from outside the repository, or a hole in the scan,
18
+ * and only the record separates them — so the record is read without waiting to
19
+ * be asked.
20
+ * - **untouched, and still** — counted, never listed. On a narrowed run it reads
21
+ * *untouched, not rendered* instead, because those subjects were ruled out
22
+ * before any browser opened them and were never compared to anything.
23
+ *
24
+ * ## A refusal is drawn instead of the grid, not beside it
25
+ *
26
+ * A run can carry a diff it could not attribute: a changed file the graph does not
27
+ * hold, a diff with nothing in the graph at all, a diff arriving at no component.
28
+ * The store keeps that apart from an attribution that genuinely reached nothing,
29
+ * and so does this. An empty grid over a refusal reads as *this commit reaches
30
+ * none of your subjects*, which is a sentence somebody merges on.
31
+ */
32
+ import { type ReactElement } from 'react';
33
+ import type { BuildDetail, ReachView, SubjectView } from '../review-types.js';
34
+ import type { ReviewClient } from './client.js';
35
+ /**
36
+ * The four states, and the two counts that keep them honest.
37
+ *
38
+ * `unplaced` and `incomparable` exist so the four quadrants add up to the suite
39
+ * without any of them absorbing a subject nobody can classify. A baseline that
40
+ * recorded no component list cannot be put on either axis, and a subject that is
41
+ * new or that nothing could compare has no *moved* to speak of. Folding either
42
+ * into the quiet quadrant would make the one nobody reads the place uncertainty
43
+ * goes to die.
44
+ */
45
+ export interface Crossing {
46
+ readonly reachedMoved: readonly SubjectView[];
47
+ readonly reachedStill: readonly SubjectView[];
48
+ readonly unreachedMoved: readonly SubjectView[];
49
+ readonly unreachedStill: number;
50
+ /**
51
+ * Out of reach and never rendered, because the run worked that out first.
52
+ *
53
+ * Apart from `unreachedStill` and never added to it. Those were opened and
54
+ * found identical; these were never opened at all, and a page that summed them
55
+ * would report a comparison for eighteen subjects no browser ever loaded.
56
+ */
57
+ readonly unreachedSkipped: number;
58
+ readonly unplaced: number;
59
+ readonly incomparable: number;
60
+ }
61
+ export declare function crossReach(subjects: readonly SubjectView[], reach: ReachView, notObserved?: BuildDetail['notObserved']): Crossing;
62
+ export declare function ReachPanel({ client, build, }: {
63
+ readonly client: ReviewClient;
64
+ readonly build: BuildDetail;
65
+ }): ReactElement | null;
66
+ //# sourceMappingURL=reach.d.ts.map
@@ -0,0 +1,185 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * What the commit reaches, crossed against what the run saw.
4
+ *
5
+ * Every other panel on this page answers *what changed*. This one answers *what
6
+ * could have*, and the whole value is in the crossing: a verdict alone cannot say
7
+ * whether a green subject was supposed to be green, and a diff alone cannot say
8
+ * whether the file somebody edited paints anything at all.
9
+ *
10
+ * Four states come out of it, and two are findings no image comparison can reach:
11
+ *
12
+ * - **reached, and moved** — the expected case, and the trail names the file.
13
+ * - **reached, and still** — the commit reaches this subject and changed no pixel
14
+ * in it. Every tool in this category is silent about a green subject, so an
15
+ * author who believed they were changing this surface learns it here or not at
16
+ * all.
17
+ * - **moved, unreached** — nothing in the commit arrives here and it moved anyway.
18
+ * That is a flake, an input from outside the repository, or a hole in the scan,
19
+ * and only the record separates them — so the record is read without waiting to
20
+ * be asked.
21
+ * - **untouched, and still** — counted, never listed. On a narrowed run it reads
22
+ * *untouched, not rendered* instead, because those subjects were ruled out
23
+ * before any browser opened them and were never compared to anything.
24
+ *
25
+ * ## A refusal is drawn instead of the grid, not beside it
26
+ *
27
+ * A run can carry a diff it could not attribute: a changed file the graph does not
28
+ * hold, a diff with nothing in the graph at all, a diff arriving at no component.
29
+ * The store keeps that apart from an attribution that genuinely reached nothing,
30
+ * and so does this. An empty grid over a refusal reads as *this commit reaches
31
+ * none of your subjects*, which is a sentence somebody merges on.
32
+ */
33
+ import { useCallback, useEffect, useState } from 'react';
34
+ import { StabilityLine } from './history.js';
35
+ import { OutcomeMapView } from './outcome.js';
36
+ import { briefly, count, number } from './text.js';
37
+ export function crossReach(subjects, reach, notObserved = []) {
38
+ const placed = reach.subjects;
39
+ const reachedMoved = [];
40
+ const reachedStill = [];
41
+ const unreachedMoved = [];
42
+ let unreachedStill = 0;
43
+ let unplaced = 0;
44
+ let incomparable = 0;
45
+ for (const subject of subjects) {
46
+ const entry = placed?.[subject.subject];
47
+ if (entry === undefined) {
48
+ unplaced += 1;
49
+ }
50
+ else if (subject.verdict === 'changed') {
51
+ (entry.reached ? reachedMoved : unreachedMoved).push(subject);
52
+ }
53
+ else if (subject.verdict === 'unchanged') {
54
+ // `changed` and `unchanged` are the only verdicts describing a pair this
55
+ // run actually compared. A new subject has no baseline to have moved from,
56
+ // and an incomparable one is a comparison that did not happen — neither
57
+ // belongs on an axis called moved.
58
+ if (entry.reached)
59
+ reachedStill.push(subject);
60
+ else
61
+ unreachedStill += 1;
62
+ }
63
+ else {
64
+ incomparable += 1;
65
+ }
66
+ }
67
+ return {
68
+ reachedMoved,
69
+ reachedStill,
70
+ unreachedMoved,
71
+ unreachedStill,
72
+ unreachedSkipped: notObserved.filter((entry) => entry.kind === 'unreached').length,
73
+ unplaced,
74
+ incomparable,
75
+ };
76
+ }
77
+ /** How many unexplained subjects read their own record without being asked. */
78
+ const ASKED = 5;
79
+ /** How many trails are drawn before the rest are counted. */
80
+ const TRAILS = 8;
81
+ export function ReachPanel({ client, build, }) {
82
+ const reach = build.reach;
83
+ if (reach === null)
84
+ return null;
85
+ return (_jsxs("section", { className: "va-card va-reach", children: [_jsx("h2", { children: "What this commit reaches" }), _jsxs("p", { className: "va-subtitle", children: [count(reach.changed.length, 'file'), " changed against ", _jsx("code", { children: reach.against }), ". The import graph carries them to ", count(reach.components.length, 'component'), "; the baselines say which subjects render those."] }), _jsx(OutcomeMapView, { build: build }), reach.whole === undefined ? (_jsx(Crossed, { client: client, build: build, reach: reach })) : (_jsxs("p", { className: "va-refusal", children: ["This diff was not attributed, so nothing here may be called unreached: ", reach.whole, "."] })), _jsx(Holes, { reach: reach })] }));
86
+ }
87
+ function Crossed({ client, build, reach, }) {
88
+ const crossing = crossReach(build.subjects, reach, build.notObserved);
89
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "va-quad", children: [_jsx(Cell, { tone: "va-expected", n: crossing.reachedMoved.length, title: "reached, and moved", note: "The commit arrives and the render changed. What the edit was for." }), _jsx(Cell, { tone: "va-alarm", n: crossing.unreachedMoved.length, title: "moved, unreached", note: "Nothing in this commit arrives here. It moved anyway." }), _jsx(Cell, { tone: "va-inert", n: crossing.reachedStill.length, title: "reached, and still", note: "The commit arrives and changed no pixel of it." }), crossing.unreachedSkipped === 0 ? (_jsx(Cell, { tone: "va-quiet", n: crossing.unreachedStill, title: "untouched, and still", note: "Out of the commit's reach, and unchanged." })) : (_jsx(Cell, { tone: "va-quiet", n: crossing.unreachedSkipped, title: "untouched, not rendered", note: "Out of the commit's reach, so this run never opened them." }))] }), crossing.unreachedMoved.length === 0 ? null : (_jsxs("section", { className: "va-band va-alarm", children: [_jsx("h3", { children: "Moved with nothing reaching it" }), _jsx("p", { className: "va-note", children: "The graph placed every changed file and none of them arrives here. Either this run read the subject differently from the last, something outside the repository moved, or the scan has a hole in it \u2014 and the record is what tells those apart." }), _jsx("ul", { className: "va-reach-list", children: crossing.unreachedMoved.map((subject, index) => (_jsxs("li", { children: [_jsx("strong", { children: subject.subject }), ' ', _jsxs("span", { className: "va-note", children: [briefly(subject), " moved"] }), index < ASKED ? _jsx(Journey, { client: client, subject: subject.subject }) : null] }, subject.subject))) }), crossing.unreachedMoved.length <= ASKED ? null : (_jsxs("p", { className: "va-note", children: ["The record was read for the first ", number(ASKED), ". Open the rest from the rail."] }))] })), crossing.reachedStill.length === 0 ? null : (_jsxs("section", { className: "va-band va-inert", children: [_jsx("h3", { children: "Reached, and did not move" }), _jsx("p", { className: "va-note", children: "The commit arrives at these and the render is identical to the baseline. Nothing else on this page says anything at all about a green subject." }), _jsx("ul", { className: "va-reach-list va-columns", children: crossing.reachedStill.map((subject) => (_jsxs("li", { children: [_jsx("strong", { children: subject.subject }), ' ', _jsx(Via, { through: reach.subjects?.[subject.subject]?.through ?? [] })] }, subject.subject))) })] })), _jsx(Trails, { reach: reach }), _jsx(Unplaced, { crossing: crossing })] }));
90
+ }
91
+ function Cell({ tone, n, title, note, }) {
92
+ // A zero is drawn quietly rather than hidden. An omitted quadrant reads as a
93
+ // question that was not asked; a zero is the answer to it.
94
+ return (_jsxs("div", { className: n === 0 ? `va-cell ${tone} va-zero` : `va-cell ${tone}`, children: [_jsx("span", { className: "va-cell-n va-num", children: number(n) }), _jsx("span", { className: "va-cell-title", children: title }), _jsx("span", { className: "va-cell-note", children: note })] }));
95
+ }
96
+ /**
97
+ * The chains, which are the part a reviewer can check.
98
+ *
99
+ * A component named without its chain is an assertion. `tokens.css →
100
+ * button.tsx → Button` is a claim somebody can open three files and disprove,
101
+ * which is the only kind worth putting on a review surface.
102
+ */
103
+ function Trails({ reach }) {
104
+ if (reach.components.length === 0)
105
+ return null;
106
+ const shown = reach.components.slice(0, TRAILS);
107
+ return (_jsxs("section", { className: "va-band", children: [_jsx("h3", { children: "How the diff arrives" }), _jsx("ul", { className: "va-trails", children: shown.map((entry) => (_jsxs("li", { children: [entry.trail.map((step, index) => (_jsxs("span", { children: [index === 0 ? null : _jsx("span", { className: "va-arrow", children: "\u2192" }), index === entry.trail.length - 1 ? _jsx("strong", { children: step }) : _jsx("code", { children: step })] }, step))), entry.throughUnread === undefined ? null : (_jsx("span", { className: "va-axis va-unread", children: "begins at a file the scan could not read" }))] }, entry.component))) }), reach.components.length <= shown.length ? null : (_jsxs("p", { className: "va-note", children: [count(reach.components.length - shown.length, 'further component'), " reached, not listed."] }))] }));
108
+ }
109
+ /**
110
+ * The subjects the crossing could not seat, said rather than absorbed.
111
+ *
112
+ * Two different silences, kept as two. A subject whose baseline carries no
113
+ * component list is one this surface cannot answer for; a new or incomparable
114
+ * subject is one where the question does not apply. Printing a single number for
115
+ * both would invite the reader to fix the wrong thing.
116
+ */
117
+ function Unplaced({ crossing }) {
118
+ if (crossing.unplaced + crossing.incomparable === 0)
119
+ return null;
120
+ return (_jsxs("p", { className: "va-note va-reach-note", children: [crossing.unplaced === 0 ? null : (_jsxs(_Fragment, { children: [count(crossing.unplaced, 'subject'), " could not be placed on either axis, because", ' ', crossing.unplaced === 1 ? 'its baseline records' : 'their baselines record', " no component list.", ' '] })), crossing.incomparable === 0 ? null : (_jsxs(_Fragment, { children: [count(crossing.incomparable, 'subject'), ' ', crossing.incomparable === 1 ? 'was' : 'were', " new or could not be compared, so there is no moved-or-still to cross."] }))] }));
121
+ }
122
+ /**
123
+ * Where the attribution is knowingly wider than the diff.
124
+ *
125
+ * Both lists name their files rather than counting them, because both are things
126
+ * an operator can go and fix, and a bare count tells them there is nothing to do.
127
+ */
128
+ function Holes({ reach }) {
129
+ if (reach.unscanned === undefined && reach.opaque === undefined)
130
+ return null;
131
+ return (_jsxs("p", { className: "va-note va-reach-note", children: [reach.unscanned === undefined ? null : (_jsxs(_Fragment, { children: [count(reach.unscanned.length, 'changed file'), " under the scanned roots", ' ', reach.unscanned.length === 1 ? 'is' : 'are', " not in the graph:", ' ', _jsx(Files, { names: reach.unscanned }), ".", ' '] })), reach.opaque === undefined ? null : (_jsxs(_Fragment, { children: [count(reach.opaque.length, 'file'), " was walked as changed because its own imports could not be read: ", _jsx(Files, { names: reach.opaque.map((hole) => hole.file) }), "."] }))] }));
132
+ }
133
+ /**
134
+ * Which of a subject's own components the commit arrives at.
135
+ *
136
+ * `SubjectReach.through` holds component names, not paths, and they are set as
137
+ * prose for that reason: a reader shown `MainNav` in the same monospace face as
138
+ * `app/src/components/MainNav.tsx` reads the two as the same kind of thing and
139
+ * starts looking for a file by that name.
140
+ *
141
+ * Nothing at all when the list is empty, rather than a dangling `via`. Empty here
142
+ * is `reached: false`, which this list is not.
143
+ */
144
+ function Via({ through }) {
145
+ if (through.length === 0)
146
+ return null;
147
+ return _jsxs("span", { className: "va-note", children: ["via ", through.join(', ')] });
148
+ }
149
+ function Files({ names }) {
150
+ return (_jsx(_Fragment, { children: names.map((name, index) => (_jsxs("span", { children: [index === 0 ? null : ', ', _jsx("code", { children: name })] }, name))) }));
151
+ }
152
+ /**
153
+ * What the record says about a subject nothing in this commit reaches.
154
+ *
155
+ * Read on mount rather than behind a button, which is the opposite of the
156
+ * per-subject history elsewhere on this page and is deliberate. A reviewer who
157
+ * has opened a subject has already decided it is worth their time; this list is
158
+ * short by construction and every row on it is *already* the anomaly. "Has this
159
+ * moved on its own before" is the entire reason the row is here, so waiting to be
160
+ * asked it would be waiting to be asked the point.
161
+ */
162
+ function Journey({ client, subject, }) {
163
+ const [answer, setAnswer] = useState({ state: 'reading' });
164
+ const load = useCallback(async () => {
165
+ try {
166
+ setAnswer({ state: 'read', flakiness: await client.flakiness(subject) });
167
+ }
168
+ catch (error) {
169
+ setAnswer({ state: 'failed', why: error instanceof Error ? error.message : String(error) });
170
+ }
171
+ }, [client, subject]);
172
+ useEffect(() => {
173
+ void load();
174
+ }, [load]);
175
+ if (answer.state === 'reading')
176
+ return _jsx("p", { className: "va-note", children: "Reading the record\u2026" });
177
+ // A record that could not be read says so. Blank here reads as "nothing on
178
+ // file", which is the one answer that would send a reviewer to approve an
179
+ // otherwise unexplained change.
180
+ if (answer.state === 'failed') {
181
+ return _jsxs("p", { className: "va-note va-lost", children: ["The record could not be read: ", answer.why] });
182
+ }
183
+ return _jsx(StabilityLine, { flakiness: answer.flakiness });
184
+ }
185
+ //# sourceMappingURL=reach.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reach.js","sourceRoot":"","sources":["../../src/ui/reach.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the commit reaches, crossed against what the run saw.\n *\n * Every other panel on this page answers *what changed*. This one answers *what\n * could have*, and the whole value is in the crossing: a verdict alone cannot say\n * whether a green subject was supposed to be green, and a diff alone cannot say\n * whether the file somebody edited paints anything at all.\n *\n * Four states come out of it, and two are findings no image comparison can reach:\n *\n * - **reached, and moved** — the expected case, and the trail names the file.\n * - **reached, and still** — the commit reaches this subject and changed no pixel\n * in it. Every tool in this category is silent about a green subject, so an\n * author who believed they were changing this surface learns it here or not at\n * all.\n * - **moved, unreached** — nothing in the commit arrives here and it moved anyway.\n * That is a flake, an input from outside the repository, or a hole in the scan,\n * and only the record separates them — so the record is read without waiting to\n * be asked.\n * - **untouched, and still** — counted, never listed. On a narrowed run it reads\n * *untouched, not rendered* instead, because those subjects were ruled out\n * before any browser opened them and were never compared to anything.\n *\n * ## A refusal is drawn instead of the grid, not beside it\n *\n * A run can carry a diff it could not attribute: a changed file the graph does not\n * hold, a diff with nothing in the graph at all, a diff arriving at no component.\n * The store keeps that apart from an attribution that genuinely reached nothing,\n * and so does this. An empty grid over a refusal reads as *this commit reaches\n * none of your subjects*, which is a sentence somebody merges on.\n */\n\nimport { useCallback, useEffect, useState, type ReactElement } from 'react';\nimport type { Flakiness } from '@variance-authority/history';\nimport type { BuildDetail, ReachView, SubjectView } from '../review-types.js';\nimport type { ReviewClient } from './client.js';\nimport { StabilityLine } from './history.js';\nimport { OutcomeMapView } from './outcome.js';\nimport { briefly, count, number } from './text.js';\n\n/**\n * The four states, and the two counts that keep them honest.\n *\n * `unplaced` and `incomparable` exist so the four quadrants add up to the suite\n * without any of them absorbing a subject nobody can classify. A baseline that\n * recorded no component list cannot be put on either axis, and a subject that is\n * new or that nothing could compare has no *moved* to speak of. Folding either\n * into the quiet quadrant would make the one nobody reads the place uncertainty\n * goes to die.\n */\nexport interface Crossing {\n readonly reachedMoved: readonly SubjectView[];\n readonly reachedStill: readonly SubjectView[];\n readonly unreachedMoved: readonly SubjectView[];\n readonly unreachedStill: number;\n /**\n * Out of reach and never rendered, because the run worked that out first.\n *\n * Apart from `unreachedStill` and never added to it. Those were opened and\n * found identical; these were never opened at all, and a page that summed them\n * would report a comparison for eighteen subjects no browser ever loaded.\n */\n readonly unreachedSkipped: number;\n readonly unplaced: number;\n readonly incomparable: number;\n}\n\nexport function crossReach(\n subjects: readonly SubjectView[],\n reach: ReachView,\n notObserved: BuildDetail['notObserved'] = [],\n): Crossing {\n const placed = reach.subjects;\n const reachedMoved: SubjectView[] = [];\n const reachedStill: SubjectView[] = [];\n const unreachedMoved: SubjectView[] = [];\n let unreachedStill = 0;\n let unplaced = 0;\n let incomparable = 0;\n\n for (const subject of subjects) {\n const entry = placed?.[subject.subject];\n if (entry === undefined) {\n unplaced += 1;\n } else if (subject.verdict === 'changed') {\n (entry.reached ? reachedMoved : unreachedMoved).push(subject);\n } else if (subject.verdict === 'unchanged') {\n // `changed` and `unchanged` are the only verdicts describing a pair this\n // run actually compared. A new subject has no baseline to have moved from,\n // and an incomparable one is a comparison that did not happen — neither\n // belongs on an axis called moved.\n if (entry.reached) reachedStill.push(subject);\n else unreachedStill += 1;\n } else {\n incomparable += 1;\n }\n }\n\n return {\n reachedMoved,\n reachedStill,\n unreachedMoved,\n unreachedStill,\n unreachedSkipped: notObserved.filter((entry) => entry.kind === 'unreached').length,\n unplaced,\n incomparable,\n };\n}\n\n/** How many unexplained subjects read their own record without being asked. */\nconst ASKED = 5;\n\n/** How many trails are drawn before the rest are counted. */\nconst TRAILS = 8;\n\nexport function ReachPanel({\n client,\n build,\n}: {\n readonly client: ReviewClient;\n readonly build: BuildDetail;\n}): ReactElement | null {\n const reach = build.reach;\n if (reach === null) return null;\n\n return (\n <section className=\"va-card va-reach\">\n <h2>What this commit reaches</h2>\n <p className=\"va-subtitle\">\n {count(reach.changed.length, 'file')} changed against <code>{reach.against}</code>. The\n import graph carries them to {count(reach.components.length, 'component')}; the baselines say\n which subjects render those.\n </p>\n\n <OutcomeMapView build={build} />\n\n {reach.whole === undefined ? (\n <Crossed client={client} build={build} reach={reach} />\n ) : (\n <p className=\"va-refusal\">\n This diff was not attributed, so nothing here may be called unreached: {reach.whole}.\n </p>\n )}\n\n <Holes reach={reach} />\n </section>\n );\n}\n\nfunction Crossed({\n client,\n build,\n reach,\n}: {\n readonly client: ReviewClient;\n readonly build: BuildDetail;\n readonly reach: ReachView;\n}): ReactElement {\n const crossing = crossReach(build.subjects, reach, build.notObserved);\n\n return (\n <>\n <div className=\"va-quad\">\n <Cell\n tone=\"va-expected\"\n n={crossing.reachedMoved.length}\n title=\"reached, and moved\"\n note=\"The commit arrives and the render changed. What the edit was for.\"\n />\n <Cell\n tone=\"va-alarm\"\n n={crossing.unreachedMoved.length}\n title=\"moved, unreached\"\n note=\"Nothing in this commit arrives here. It moved anyway.\"\n />\n <Cell\n tone=\"va-inert\"\n n={crossing.reachedStill.length}\n title=\"reached, and still\"\n note=\"The commit arrives and changed no pixel of it.\"\n />\n {crossing.unreachedSkipped === 0 ? (\n <Cell\n tone=\"va-quiet\"\n n={crossing.unreachedStill}\n title=\"untouched, and still\"\n note=\"Out of the commit's reach, and unchanged.\"\n />\n ) : (\n // The run narrowed to what the diff can arrive at, so these were never\n // opened. Drawn as *not rendered* rather than *unchanged*: nothing\n // compared them, and the quadrant that says otherwise would be the one\n // place on the page claiming a result for work nobody did.\n <Cell\n tone=\"va-quiet\"\n n={crossing.unreachedSkipped}\n title=\"untouched, not rendered\"\n note=\"Out of the commit's reach, so this run never opened them.\"\n />\n )}\n </div>\n\n {crossing.unreachedMoved.length === 0 ? null : (\n <section className=\"va-band va-alarm\">\n <h3>Moved with nothing reaching it</h3>\n <p className=\"va-note\">\n The graph placed every changed file and none of them arrives here. Either this run read\n the subject differently from the last, something outside the repository moved, or the\n scan has a hole in it — and the record is what tells those apart.\n </p>\n <ul className=\"va-reach-list\">\n {crossing.unreachedMoved.map((subject, index) => (\n <li key={subject.subject}>\n <strong>{subject.subject}</strong>{' '}\n <span className=\"va-note\">\n {briefly(subject)} moved\n </span>\n {index < ASKED ? <Journey client={client} subject={subject.subject} /> : null}\n </li>\n ))}\n </ul>\n {crossing.unreachedMoved.length <= ASKED ? null : (\n <p className=\"va-note\">\n The record was read for the first {number(ASKED)}. Open the rest from the rail.\n </p>\n )}\n </section>\n )}\n\n {crossing.reachedStill.length === 0 ? null : (\n <section className=\"va-band va-inert\">\n <h3>Reached, and did not move</h3>\n <p className=\"va-note\">\n The commit arrives at these and the render is identical to the baseline. Nothing else on\n this page says anything at all about a green subject.\n </p>\n <ul className=\"va-reach-list va-columns\">\n {crossing.reachedStill.map((subject) => (\n <li key={subject.subject}>\n <strong>{subject.subject}</strong>{' '}\n <Via through={reach.subjects?.[subject.subject]?.through ?? []} />\n </li>\n ))}\n </ul>\n </section>\n )}\n\n <Trails reach={reach} />\n <Unplaced crossing={crossing} />\n </>\n );\n}\n\nfunction Cell({\n tone,\n n,\n title,\n note,\n}: {\n readonly tone: string;\n readonly n: number;\n readonly title: string;\n readonly note: string;\n}): ReactElement {\n // A zero is drawn quietly rather than hidden. An omitted quadrant reads as a\n // question that was not asked; a zero is the answer to it.\n return (\n <div className={n === 0 ? `va-cell ${tone} va-zero` : `va-cell ${tone}`}>\n <span className=\"va-cell-n va-num\">{number(n)}</span>\n <span className=\"va-cell-title\">{title}</span>\n <span className=\"va-cell-note\">{note}</span>\n </div>\n );\n}\n\n/**\n * The chains, which are the part a reviewer can check.\n *\n * A component named without its chain is an assertion. `tokens.css →\n * button.tsx → Button` is a claim somebody can open three files and disprove,\n * which is the only kind worth putting on a review surface.\n */\nfunction Trails({ reach }: { readonly reach: ReachView }): ReactElement | null {\n if (reach.components.length === 0) return null;\n const shown = reach.components.slice(0, TRAILS);\n\n return (\n <section className=\"va-band\">\n <h3>How the diff arrives</h3>\n <ul className=\"va-trails\">\n {shown.map((entry) => (\n <li key={entry.component}>\n {entry.trail.map((step, index) => (\n <span key={step}>\n {index === 0 ? null : <span className=\"va-arrow\">→</span>}\n {index === entry.trail.length - 1 ? <strong>{step}</strong> : <code>{step}</code>}\n </span>\n ))}\n {entry.throughUnread === undefined ? null : (\n <span className=\"va-axis va-unread\">begins at a file the scan could not read</span>\n )}\n </li>\n ))}\n </ul>\n {reach.components.length <= shown.length ? null : (\n <p className=\"va-note\">\n {count(reach.components.length - shown.length, 'further component')} reached, not listed.\n </p>\n )}\n </section>\n );\n}\n\n/**\n * The subjects the crossing could not seat, said rather than absorbed.\n *\n * Two different silences, kept as two. A subject whose baseline carries no\n * component list is one this surface cannot answer for; a new or incomparable\n * subject is one where the question does not apply. Printing a single number for\n * both would invite the reader to fix the wrong thing.\n */\nfunction Unplaced({ crossing }: { readonly crossing: Crossing }): ReactElement | null {\n if (crossing.unplaced + crossing.incomparable === 0) return null;\n\n return (\n <p className=\"va-note va-reach-note\">\n {crossing.unplaced === 0 ? null : (\n <>\n {count(crossing.unplaced, 'subject')} could not be placed on either axis, because{' '}\n {crossing.unplaced === 1 ? 'its baseline records' : 'their baselines record'} no component\n list.{' '}\n </>\n )}\n {crossing.incomparable === 0 ? null : (\n <>\n {count(crossing.incomparable, 'subject')}{' '}\n {crossing.incomparable === 1 ? 'was' : 'were'} new or could not be compared, so there is\n no moved-or-still to cross.\n </>\n )}\n </p>\n );\n}\n\n/**\n * Where the attribution is knowingly wider than the diff.\n *\n * Both lists name their files rather than counting them, because both are things\n * an operator can go and fix, and a bare count tells them there is nothing to do.\n */\nfunction Holes({ reach }: { readonly reach: ReachView }): ReactElement | null {\n if (reach.unscanned === undefined && reach.opaque === undefined) return null;\n\n return (\n <p className=\"va-note va-reach-note\">\n {reach.unscanned === undefined ? null : (\n <>\n {count(reach.unscanned.length, 'changed file')} under the scanned roots{' '}\n {reach.unscanned.length === 1 ? 'is' : 'are'} not in the graph:{' '}\n <Files names={reach.unscanned} />.{' '}\n </>\n )}\n {reach.opaque === undefined ? null : (\n <>\n {count(reach.opaque.length, 'file')} was walked as changed because its own imports could\n not be read: <Files names={reach.opaque.map((hole) => hole.file)} />.\n </>\n )}\n </p>\n );\n}\n\n/**\n * Which of a subject's own components the commit arrives at.\n *\n * `SubjectReach.through` holds component names, not paths, and they are set as\n * prose for that reason: a reader shown `MainNav` in the same monospace face as\n * `app/src/components/MainNav.tsx` reads the two as the same kind of thing and\n * starts looking for a file by that name.\n *\n * Nothing at all when the list is empty, rather than a dangling `via`. Empty here\n * is `reached: false`, which this list is not.\n */\nfunction Via({ through }: { readonly through: readonly string[] }): ReactElement | null {\n if (through.length === 0) return null;\n\n return <span className=\"va-note\">via {through.join(', ')}</span>;\n}\n\nfunction Files({ names }: { readonly names: readonly string[] }): ReactElement {\n return (\n <>\n {names.map((name, index) => (\n <span key={name}>\n {index === 0 ? null : ', '}\n <code>{name}</code>\n </span>\n ))}\n </>\n );\n}\n\n/**\n * What the record says about a subject nothing in this commit reaches.\n *\n * Read on mount rather than behind a button, which is the opposite of the\n * per-subject history elsewhere on this page and is deliberate. A reviewer who\n * has opened a subject has already decided it is worth their time; this list is\n * short by construction and every row on it is *already* the anomaly. \"Has this\n * moved on its own before\" is the entire reason the row is here, so waiting to be\n * asked it would be waiting to be asked the point.\n */\nfunction Journey({\n client,\n subject,\n}: {\n readonly client: ReviewClient;\n readonly subject: string;\n}): ReactElement {\n const [answer, setAnswer] = useState<\n | { readonly state: 'reading' }\n | { readonly state: 'failed'; readonly why: string }\n | { readonly state: 'read'; readonly flakiness: Flakiness }\n >({ state: 'reading' });\n\n const load = useCallback(async (): Promise<void> => {\n try {\n setAnswer({ state: 'read', flakiness: await client.flakiness(subject) });\n } catch (error) {\n setAnswer({ state: 'failed', why: error instanceof Error ? error.message : String(error) });\n }\n }, [client, subject]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n if (answer.state === 'reading') return <p className=\"va-note\">Reading the record…</p>;\n\n // A record that could not be read says so. Blank here reads as \"nothing on\n // file\", which is the one answer that would send a reviewer to approve an\n // otherwise unexplained change.\n if (answer.state === 'failed') {\n return <p className=\"va-note va-lost\">The record could not be read: {answer.why}</p>;\n }\n\n return <StabilityLine flakiness={answer.flakiness} />;\n}\n"]}
@@ -0,0 +1,82 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { SubjectView } from '../review.js';
3
+ /**
4
+ * The boxes, and the table that names them.
5
+ *
6
+ * Apart from [`viewer.tsx`](./viewer.tsx) because the stage is one question — how
7
+ * do I look at two images — and this is the other: *which of these boxes is the
8
+ * one somebody edited, and who owns it*. The two are drawn together and read
9
+ * together, and a reviewer moves between them constantly, which is why the focus
10
+ * is one index shared by both rather than a hover state in each.
11
+ *
12
+ * The row and the rectangle are the same finding twice. Pointing at either lights
13
+ * the other, because a rectangle 700 pixels down a route capture and a row in a
14
+ * table are otherwise two lists a reader has to join by counting.
15
+ */
16
+ export interface RegionFocus {
17
+ /** Which region is lit, by report order. `null` is none. */
18
+ readonly focus: number | null;
19
+ readonly onFocus: (index: number | null) => void;
20
+ /** Bring this region into view on the stage. */
21
+ readonly onJump: (index: number) => void;
22
+ }
23
+ /**
24
+ * Region rectangles over the candidate, cause and collateral drawn apart.
25
+ *
26
+ * Positioned in percentages of the raster's own dimensions, which the build
27
+ * carries, rather than of the rendered element. Measuring the image in the
28
+ * browser would place every box correctly only after it had loaded, and wrongly
29
+ * for the frame before that — and a box in the wrong place is worse than no box,
30
+ * because it attributes a change to whatever it happens to land on.
31
+ *
32
+ * Percentages are also what survives the zoom: the stage scales the plate and
33
+ * every box scales with it, so nothing here has to know the magnification.
34
+ *
35
+ * Only the causes and whatever is under attention are named on the picture. Thirty
36
+ * collateral captions over a page fitted to a pane overlap into a grey field, and
37
+ * a reviewer who cannot read any of them is worse off than one reading two — so
38
+ * the rest carry their ordinal, which is the number they are listed under, and the
39
+ * sentence arrives on hover and in the table. Nothing is dropped: every region
40
+ * that was found is a box and a row, both of them always.
41
+ */
42
+ export declare function RegionOverlay({ subject, box, focus, onFocus, }: {
43
+ readonly subject: SubjectView;
44
+ /**
45
+ * The frame the percentages are of, when it is not the candidate's own.
46
+ *
47
+ * Region coordinates are in the comparison's space, and a comparison pads to
48
+ * the union of the two captures — so on a subject whose baseline was a
49
+ * different size, the candidate's dimensions are the wrong denominator and
50
+ * every box lands short by the difference.
51
+ */
52
+ readonly box?: {
53
+ readonly width: number;
54
+ readonly height: number;
55
+ } | undefined;
56
+ readonly focus?: number | null;
57
+ readonly onFocus?: ((index: number | null) => void) | undefined;
58
+ }): ReactElement | null;
59
+ /**
60
+ * The same regions as rows, which is the half a picture cannot deliver.
61
+ *
62
+ * A rectangle says *here*. It cannot say `src/ui/Button.tsx:18`, and the file is
63
+ * the column that lets a reviewer hand the change to whoever owns it rather than
64
+ * deciding it themselves. Ordered as the report ordered them — causes first,
65
+ * never by area, which reports the container that reflowed instead of the
66
+ * component that was edited.
67
+ */
68
+ export declare function RegionTable({ subject, sourced, focus, onFocus, onJump, }: {
69
+ readonly subject: SubjectView;
70
+ /**
71
+ * Whether the *run* resolved any source file at all.
72
+ *
73
+ * A property of the run's source index, not of one capture, and the caller who
74
+ * holds the build is the only one who can say it. Left out, this falls back to
75
+ * what one render can see on its own, which is strictly weaker: a capture whose
76
+ * every region belongs to a dependency resolves nothing and would report a run
77
+ * with no index — the same name then reads two ways on two surfaces, and a
78
+ * reviewer takes one of them for a bug.
79
+ */
80
+ readonly sourced?: boolean | undefined;
81
+ } & RegionFocus): ReactElement | null;
82
+ //# sourceMappingURL=regions.d.ts.map