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