@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 @@
1
+ {"version":3,"file":"change-story.js","sourceRoot":"","sources":["../../src/ui/change-story.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAqB,MAAM,OAAO,CAAC;AAK5E,OAAO,EAAE,QAAQ,EAAe,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAEnD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAE1C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,KAAK,CAAC,EACpB,MAAM,EACN,SAAS,EACT,IAAI,GAOL;IACC,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC;IACjD,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,GAAG,SAAS,CAAC;IAEnD,OAAO,CACL,aAAG,SAAS,EAAC,UAAU,aACrB,gBAAM,KAAK,EAAC,qEAAqE,aAC/E,2BAAS,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAU,sBACnC,EACP,eAAM,KAAK,EAAC,wIAAmI,YAC5I,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACd,8BACE,wCAAyB,uEACxB,CACJ,CAAC,CAAC,CAAC,CACF,8BACE,2BAAS,KAAK,CAAC,MAAM,EAAE,YAAY,CAAC,GAAU,kBAC7C,CACJ,GACI,EACP,eAAM,KAAK,EAAC,yFAAoF,YAC7F,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CACZ,8BACE,uCAAwB,uBACvB,CACJ,CAAC,CAAC,CAAC,CACF,8BACE,2BAAS,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAU,kBACvC,CACJ,GACI,EACP,gBAAM,SAAS,EAAC,SAAS,aAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,IACzD,CACL,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,EACvB,IAAI,EACJ,OAAO,GAIR;IACC,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,eAAM,SAAS,EAAC,SAAS,YAAE,IAAI,GAAQ,CAAC;IAEvE,OAAO,CACL,eACE,SAAS,EAAC,SAAS,EACnB,KAAK,EACH,OAAO;YACL,CAAC,CAAC,mLAAmL;YACrL,CAAC,CAAC,4GAA4G,YAGjH,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,iBAAiB,GACrD,CACR,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,OAAO,CAAC,EAAE,MAAM,EAA+B;IAC7D,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAE9C,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QAC3B,OAAO,CACL,YAAG,SAAS,EAAC,uBAAuB,YACjC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,CACrB,yBAAyB,CAC1B,CAAC,CAAC,CAAC,CACF,yDAC0B,GAAG,EAC1B,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAC1B,2BACG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,UAAU,uBAAS,EACxD,KAAK,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,2BAAS,IAAI,GAAU,CAAC,CAAC,CAAC,yBAAO,IAAI,GAAQ,KAFlE,IAAI,CAGR,CACR,CAAC,IACD,CACJ,GACC,CACL,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC;IACvC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC;IACvC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;IAErC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,CACL,aAAG,SAAS,EAAC,qBAAqB,uDACO,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,yBAAgB,GAAG,EACzF,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAC/B,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,mEAExE,CACL,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,+EAA+E;IAC/E,+EAA+E;IAC/E,gFAAgF;IAChF,8EAA8E;IAC9E,wCAAwC;IACxC,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACpC,YAAG,SAAS,EAAC,uBAAuB,YAClC,KAAC,QAAQ,IAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,GAAI,GACpC,CACL,CAAC;IACJ,CAAC;IAED,OAAO,CACL,aAAG,SAAS,EAAC,uBAAuB,mDACA,2BAAS,MAAM,CAAC,SAAS,GAAU,sHACa,GAAG,EACpF,OAAO,CAAC,MAAM,KAAK,CAAC;gBACnB,CAAC,CAAC,sFAAsF;gBACxF,CAAC,CAAC,qDAAqD,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GACjF,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EACtE,kGAAkG,EACrG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC9B,8BACG,GAAG,EACJ,KAAC,QAAQ,IAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,GAAI,IACrC,CACJ,IACC,CACL,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAS,QAAQ,CAAC,EAAE,OAAO,EAAgC;IACzD,OAAO,CACL,8BACG,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,mBAAe,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,uFACV,GAAG,EAC9D,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,SAC7B,CACJ,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CAAC,EACxB,QAAQ,EACR,MAAM,GAIP;IACC,IAAI,QAAQ,CAAC,KAAK,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAE5C,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW;SAC5B,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;SAClD,MAAM,CAAC,CAAC,GAAG,EAAkB,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC;IACtD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEnC,MAAM,OAAO,GAAG,SAAS,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IAClD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC;IAC7E,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,aAAa,CAAC,CAAC,MAAM,CAAC;IACvE,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;IACjE,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,MAAM,CAAC;IAE/D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEhF,OAAO,CACL,aAAG,SAAS,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,UAAU,aAC7D,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC1B,8BACG,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,GAAG,qCACzD,OAAO,EACtC,SAAS,KAAK,CAAC;wBACd,CAAC,CAAC,oCAAoC;wBACtC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,MAAM;4BACzB,CAAC,CAAC,uCAAuC;4BACzC,CAAC,CAAC,SAAS,MAAM,CAAC,SAAS,CAAC,gCAAgC,EAAE,GAAG,IACpE,CACJ,EACA,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACpB,8BACG,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC,mBAAe,OAAO,0FACK,GAAG,IACpD,CACJ,EACA,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACpB,8BACG,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC,iCAA6B,OAAO,eAAW,GAAG,EACxE,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,yBAAqB,GAAG,IACnD,CACJ,EACA,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,8BAAG,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC,wBAAoB,OAAO,SAAK,IAC7E,CACL,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,KAAK,CAAC,KAAa;IAC1B,OAAO,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC;AAC/D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,MAAM,CAAC,EAAE,MAAM,EAA+B;IAC5D,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC9C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAErC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAClF,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,CACL,aAAG,SAAS,EAAC,SAAS,yDACqB,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,OAAG,GAAG,EACzF,gBAAM,KAAK,EAAE,IAAI,CAAC,KAAK,yCAA2B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAQ,gCAEzE,CACL,CAAC;IACJ,CAAC;IAED,OAAO,CACL,mBAAS,SAAS,EAAC,WAAW,aAC5B,yBACG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YAAQ,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,IAC7E,EACL,uBACG,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACpB,yBACE,aAAG,SAAS,EAAC,gBAAgB,aAC3B,gBAAM,SAAS,EAAC,QAAQ,aAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAC/D,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,gBAAgB,YAAE,IAAI,CAAC,IAAI,GAAQ,EACrF,gBAAM,SAAS,EAAC,gBAAgB,EAAC,KAAK,EAAE,IAAI,CAAC,KAAK,yBACvC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IACrB,IACL,EACJ,YAAG,SAAS,EAAC,sBAAsB,YAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAK,KAR1D,IAAI,CAAC,KAAK,CASd,CACN,CAAC,GACC,EACJ,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC9B,aAAG,SAAS,EAAC,SAAS,aACnB,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,wEACjB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,SACjC,CACL,IACO,CACX,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,SAAS,IAAI,CAAC,KAAa,EAAE,IAAY;IACvC,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;AAC7E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,EACzB,MAAM,EACN,SAAS,GAIV;IACC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IAEvD,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,IAAmB,EAAE;QACjD,IAAI,CAAC;YACH,QAAQ,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;QAC1C,CAAC;QAAC,MAAM,CAAC;YACP,0EAA0E;YAC1E,2EAA2E;YAC3E,kCAAkC;YAClC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IAExB,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,IAAI,EAAE,CAAC;IACd,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,2EAA2E;IAC3E,4EAA4E;IAC5E,4DAA4D;IAC5D,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEpD,OAAO,KAAC,SAAS,IAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,GAAI,CAAC;AAC3D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,EAAE,KAAK,EAAmC;IACnE,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,gBAAgB,CAAC;IACjD,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEtD,OAAO,CACL,aAAG,SAAS,EAAC,4BAA4B,aACtC,KAAK,CAAC,MAAM,EAAE,kBAAkB,CAAC,kKAEhC,CACL,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,QAAQ;IACtB,OAAO,CACL,aAAG,SAAS,EAAC,aAAa,wDACW,GAAG,EACrC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAC/D,2BACG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EACxB,uBAAK,OAAO,CAAC,IAAI,CAAC,GAAM,YAAO,KAAK,CAAC,IAAI,CAAC,KAFjC,IAAI,CAGR,CACR,CAAC,2HAGA,CACL,CAAC;AACJ,CAAC","sourcesContent":["/**\n * What the record says about one change, before anybody decides it.\n *\n * Seven readings, and not one of them is an act. [`change.tsx`](./change.tsx)\n * holds the page and the two buttons; this holds every sentence printed above\n * them, which is the half that has to be argued rather than wired — what counts\n * as *reached*, what a silence is allowed to imply, and which of two absences a\n * reviewer is looking at. Splitting them keeps the argument readable and stops\n * the page that carries the decision from being mostly prose about it.\n *\n * The rule they share: absent is not empty. A run that read no diff, a component\n * the source index does not declare, a render nobody wrote a reach row for and a\n * build that counted no collateral each get their own sentence or no sentence at\n * all, because the alternative is spending this product’s loudest line on its\n * most ordinary fact and training the reader out of it.\n */\n\nimport { useCallback, useEffect, useState, type ReactElement } from 'react';\nimport type { Churn } from '@variance-authority/history';\nimport type { BuildDetail } from '../review-types.js';\nimport type { ReviewClient } from './client.js';\nimport type { Crossing } from './crossing.js';\nimport { shapesOf, type Origin } from './grouping.js';\nimport { partedBy } from './parted.js';\nimport { ChurnLine } from './history.js';\nimport { ORDER, READS, senseOf } from './sense.js';\nimport type { Shifted } from './shift.js';\nimport { count, number } from './text.js';\n\n/**\n * Three counts, because the reviewer asks three questions and they differ.\n *\n * *2 distinct differences, in 7 renders* is two of them run together, and the\n * third — how many of those a press of the button actually settles — was in the\n * button's own label, at the other end of the header. The three come apart on\n * every change of any size: a component moves in twelve renders, five of which\n * the docket filed under somebody else; the twelve carry two distinct shapes, so\n * there are two pictures to look at rather than twelve; and seven are open here,\n * because the rest are decided or belong to the other change.\n *\n * Kept as three cells with the reviewer's own words on them rather than folded\n * into a sentence. A sentence has to pick an order and an emphasis, and these\n * are three answers of equal standing.\n *\n * The pixel total rides along, muted. It is the least useful figure available —\n * one number for every size of change — and it is the one somebody eventually\n * asks for.\n */\nexport function Tally({\n origin,\n elsewhere,\n open,\n}: {\n readonly origin: Origin;\n /** Renders it moved in that another change owns, and is decided under. */\n readonly elsewhere: number;\n /** Renders here a press settles: undecided, and approvable. */\n readonly open: number;\n}): ReactElement {\n const shapes = shapesOf(origin.appearances).size;\n const know = origin.appearances.length + elsewhere;\n\n return (\n <p className=\"va-tally\">\n <span title=\"Every render this component moved in, wherever the docket filed it.\">\n <strong>{count(know, 'render')}</strong> to know about\n </span>\n <span title=\"Distinct shapes among the renders on this page — how many pictures there are to look at, rather than how many renders carry them.\">\n {shapes === 0 ? (\n <>\n <strong>no shape</strong> was recorded, so nothing says whether these moved the same way\n </>\n ) : (\n <>\n <strong>{count(shapes, 'difference')}</strong> to review\n </>\n )}\n </span>\n <span title=\"Renders on this page that are open and approvable — what pressing approve settles.\">\n {open === 0 ? (\n <>\n <strong>nothing</strong> left to accept\n </>\n ) : (\n <>\n <strong>{count(open, 'render')}</strong> to accept\n </>\n )}\n </span>\n <span className=\"va-note\">{number(origin.pixels)} px</span>\n </p>\n );\n}\n\n/**\n * Where this component is declared, and what it means when nothing says.\n *\n * A blank is the one answer this cell must not give. The reviewer's next move on\n * a change they do not recognise is to open the file, and *nothing here* reads as\n * a defect in the tool — when the ordinary cause is that the name belongs to a\n * dependency, which the source index scans no part of and never claimed to. So\n * the two silences are separated: a run that resolved files for other components\n * has said something about this one, and a run that resolved none has not.\n */\nexport function Declared({\n file,\n sourced,\n}: {\n readonly file?: string | undefined;\n readonly sourced: boolean;\n}): ReactElement {\n if (file !== undefined) return <code className=\"va-file\">{file}</code>;\n\n return (\n <span\n className=\"va-note\"\n title={\n sourced\n ? 'This run resolved files for other components in this build, so this is a name its source index does not declare — a component out of a dependency, or one produced at build time.'\n : 'This run resolved no source files at all, so nothing here says where any of these components are declared.'\n }\n >\n {sourced ? 'not in the scanned source' : 'no source index'}\n </span>\n );\n}\n\n/**\n * Whether the commit arrives here, said in five states rather than two.\n *\n * A run with no diff read cannot say, and saying nothing is the only honest\n * version of that. Silence would be indistinguishable from *reached*, which is\n * the assumption a reviewer makes by default and the one that costs them.\n *\n * The last of the five is now conditional: it is a reconstruction, and it stands\n * down where the run recorded a conclusion of its own. See the branch below.\n *\n * The rest is the split this card got wrong twice. `reached` is computed against\n * the components the diff can arrive at, so a component the graph carries nowhere\n * — anything out of `node_modules`, which is most of the host nodes on a real\n * page — can never be in that set whatever the commit did. And a render the reach\n * record has no row for is not a render the commit fails to reach; it is a render\n * nobody wrote down. Printing *nothing reaches it and it moved anyway* over\n * either of those spends the loudest sentence on the page on the most ordinary\n * fact about it, and by the time a real orphan appears the sentence has been\n * trained out of the reader.\n */\nexport function Arrival({ origin }: { readonly origin: Origin }): ReactElement | null {\n if (origin.reached === undefined) return null;\n\n if (origin.reached) {\n const trail = origin.trail;\n return (\n <p className=\"va-reaches va-reached\">\n {trail === undefined ? (\n 'This commit reaches it.'\n ) : (\n <>\n This commit reaches it:{' '}\n {trail.map((step, index) => (\n <span key={step}>\n {index === 0 ? null : <span className=\"va-arrow\">→</span>}\n {index === trail.length - 1 ? <strong>{step}</strong> : <code>{step}</code>}\n </span>\n ))}\n </>\n )}\n </p>\n );\n }\n\n const stranded = origin.stranded ?? [];\n const unlisted = origin.unlisted ?? [];\n const through = origin.through ?? [];\n\n if (stranded.length > 0) {\n return (\n <p className=\"va-reaches va-alarm\">\n This commit reaches nothing at all in {count(stranded.length, 'render')} it moved in —{' '}\n {stranded.slice(0, 3).join(', ')}\n {stranded.length > 3 ? `, and ${count(stranded.length - 3, 'other')}` : ''}. Something\n changed there that the diff cannot account for.\n </p>\n );\n }\n\n // The run's own conclusion supersedes the rest of this, and `Because` has\n // already printed it directly above. What follows is a reconstruction from the\n // reach record alone — and on a component the import walk merely climbed past,\n // it reads *this comes from a dependency* about a file named at the top of this\n // same page. The one thing it says that the conclusion does not is the gap in\n // the record, so that is what survives.\n if (origin.cause !== undefined) {\n return unlisted.length === 0 ? null : (\n <p className=\"va-reaches va-unnamed\">\n <Unlisted renders={unlisted.length} />\n </p>\n );\n }\n\n return (\n <p className=\"va-reaches va-unnamed\">\n The commit reaches nothing called <strong>{origin.component}</strong> — the file graph carries\n no such name, which is what a component out of a dependency looks like from here.{' '}\n {through.length === 0\n ? 'It does reach every render this moved in, without naming a component in any of them.'\n : `It does reach every render this moved in, through ${through.slice(0, 4).join(', ')}${\n through.length > 4 ? ` and ${count(through.length - 4, 'other')}` : ''\n } — so either one of those drew this node, or something reaches it that the graph does not model.`}\n {unlisted.length === 0 ? null : (\n <>\n {' '}\n <Unlisted renders={unlisted.length} />\n </>\n )}\n </p>\n );\n}\n\n/**\n * The renders the reach record has no row for.\n *\n * Not *the commit does not reach these*. Nobody wrote them down, and a page that\n * folded the two together would report a gap in its own record as a finding about\n * the commit.\n */\nfunction Unlisted({ renders }: { readonly renders: number }): ReactElement {\n return (\n <>\n {count(renders, 'render')} it moved in {renders === 1 ? 'has' : 'have'} no reach recorded at\n all, so nothing the commit reaches was ever weighed against{' '}\n {renders === 1 ? 'it' : 'them'}.\n </>\n );\n}\n\n/**\n * What the run before this one had to say about these renders.\n *\n * The sentence a reviewer skips work on, and the reason it is here rather than in\n * the crossing panel: *build 5 already showed you this, and nobody decided it* is\n * worth reading beside the change it is about and worth nothing at the bottom of\n * a page about twenty other subjects. The undecided count is the part that is\n * never printed anywhere else — it is the docket being delivered twice.\n */\nexport function SinceLast({\n crossing,\n origin,\n}: {\n readonly crossing: Crossing;\n readonly origin: Origin;\n}): ReactElement | null {\n if (crossing.state !== 'ready') return null;\n\n const rows = origin.appearances\n .map(({ subject }) => crossing.of(subject.subject))\n .filter((row): row is Shifted => row !== undefined);\n if (rows.length === 0) return null;\n\n const against = `build ${crossing.earlier.build}`;\n const seen = rows.filter((row) => row.shift === 'again');\n const undecided = seen.filter((row) => row.earlier?.decision == null).length;\n const moved = rows.filter((row) => row.shift === 'differently').length;\n const first = rows.filter((row) => row.shift === 'first').length;\n const fresh = rows.filter((row) => row.shift === 'new').length;\n\n if (seen.length === 0 && moved === 0 && first === 0 && fresh === 0) return null;\n\n return (\n <p className={seen.length > 0 ? 'va-since va-known' : 'va-since'}>\n {seen.length === 0 ? null : (\n <>\n {seen.length === rows.length ? 'Every one of these' : count(seen.length, 'of these')}{' '}\n carried the same difference in {against}\n {undecided === 0\n ? ', and every one was decided there.'\n : undecided === seen.length\n ? ', and none of them was decided there.'\n : `, and ${number(undecided)} of them went undecided there.`}{' '}\n </>\n )}\n {moved === 0 ? null : (\n <>\n {count(moved, 'render')} differed in {against} too, but not in the same way: the\n difference itself changed between the two builds.{' '}\n </>\n )}\n {first === 0 ? null : (\n <>\n {count(first, 'render')} moved for the first time; {against} compared{' '}\n {first === 1 ? 'it' : 'them'} and found nothing.{' '}\n </>\n )}\n {fresh === 0 ? null : <>{count(fresh, 'render')} did not exist in {against}.</>}\n </p>\n );\n}\n\n/**\n * A shape at reading length, with the whole of it a hover away.\n *\n * The digest is thirty-two hexadecimal characters and a reviewer needs none of\n * them: it is an identity, and an identity is only ever typed by copying it. A\n * prefix says *this one and not that one* on a page that never shows two, which\n * is the whole of the job the full string was doing on the surface.\n */\nfunction brief(shape: string): string {\n return shape.length <= 14 ? shape : `${shape.slice(0, 14)}…`;\n}\n\n/**\n * Where this change's renders stopped agreeing with each other.\n *\n * A change is approved once and lands many times, and the count above reads as\n * one thing having happened seven times. On a component with two shapes it is\n * two things: two renders moved by 6,418 pixels and five by about 3,500, and a\n * reviewer pressing approve is signing off on both under one name.\n *\n * So the groups are drawn, largest first, with what each one is worth in pixels\n * and the name its renders share when that name is theirs alone. The digest goes\n * beside each group rather than in the prose, because `variance accept --shape`\n * takes exactly that set in the next run and a reviewer who has decided one of\n * the two groups is holding the argument for it.\n */\nexport function Shapes({ origin }: { readonly origin: Origin }): ReactElement | null {\n const { shapes, unshaped } = partedBy(origin);\n if (shapes.length === 0) return null;\n\n const only = shapes.length === 1 && unshaped.length === 0 ? shapes[0] : undefined;\n if (only !== undefined) {\n return (\n <p className=\"va-note\">\n The same difference in every one of the {count(origin.appearances.length, 'render')}.{' '}\n <code title={only.shape}>variance accept --shape {brief(only.shape)}</code> takes exactly\n this set.\n </p>\n );\n }\n\n return (\n <section className=\"va-parted\">\n <h2>\n {count(shapes.length, 'shape')} over {count(origin.appearances.length, 'render')}\n </h2>\n <ul>\n {shapes.map((each) => (\n <li key={each.shape}>\n <p className=\"va-parted-head\">\n <span className=\"va-num\">{span(each.least, each.most)} px</span>\n {each.only === undefined ? null : <span className=\"va-parted-only\">{each.only}</span>}\n <code className=\"va-parted-take\" title={each.shape}>\n --shape {brief(each.shape)}\n </code>\n </p>\n <p className=\"va-parted-in va-note\">{each.renders.join(', ')}</p>\n </li>\n ))}\n </ul>\n {unshaped.length === 0 ? null : (\n <p className=\"va-note\">\n {count(unshaped.length, 'render')} recorded no shape, so they are not known to match\n anything here: {unshaped.join(', ')}.\n </p>\n )}\n </section>\n );\n}\n\n/** One figure when the group is flat, two when the renders differ in size. */\nfunction span(least: number, most: number): string {\n return least === most ? number(least) : `${number(least)}–${number(most)}`;\n}\n\n/**\n * How often this component moves, asked on mount: there is one change on this page.\n *\n * Named for the question rather than the call, and not `Record`, which is a\n * global type every other file in this package is entitled to use.\n */\nexport function Recurrence({\n client,\n component,\n}: {\n readonly client: ReviewClient;\n readonly component: string;\n}): ReactElement | null {\n const [churn, setChurn] = useState<Churn | null>(null);\n\n const load = useCallback(async (): Promise<void> => {\n try {\n setChurn(await client.churn(component));\n } catch {\n // A missing record is a silence this page can afford. Everything above it\n // is a reading of this build, and none of it becomes less true because the\n // history service did not answer.\n setChurn(null);\n }\n }, [client, component]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n // `0 of 0 runs` on every change is the shape of an answer with none of the\n // substance, and this is a reading of *this* build — history is an addition\n // where there is history, not a line that has to be filled.\n if (churn === null || churn.runs === 0) return null;\n\n return <ChurnLine component={component} churn={churn} />;\n}\n\n/**\n * Collateral, and nothing at all when the build did not count it.\n *\n * One number for the whole build, repeated onto every cause row by the store —\n * which is exactly why it is printed once, here, and never attributed to a\n * change. Deciding which edit pushed which box around is the attribution the\n * semantic tier declined to make. The guard is the rule: a build with no causes\n * has not counted zero collateral pixels, it has counted none, and `0 collateral\n * pixels` reads as a measurement.\n */\nexport function Collateral({ build }: { readonly build: BuildDetail }): ReactElement | null {\n const pixels = build.causes[0]?.collateralPixels;\n if (pixels === undefined || pixels === 0) return null;\n\n return (\n <p className=\"va-note va-collateral-note\">\n {count(pixels, 'collateral pixel')} moved in this build alongside the changes — regions that\n shifted because something else did. Counted for the build, never split between the changes.\n </p>\n );\n}\n\n/**\n * What a band claim is made of, said once on the page that makes them.\n *\n * Asked directly, about `what it announces`: *how do you know?* The page had no\n * answer on it. It knows because two digests disagree, and the digest is over the\n * role, name and state of every node inside the boundary — which is enough to\n * assert the band and not enough to say what the name became, because the\n * baseline sidecar stores the hash and not the value.\n *\n * At the foot rather than beside every band, because it is the same sentence for\n * all of them and a reviewer needs it once. Not behind a disclosure: the whole\n * complaint that produced it was a page processing its results and hiding what\n * they were made from.\n */\nexport function Evidence(): ReactElement {\n return (\n <p className=\"va-evidence\">\n Each band is a digest comparison —{' '}\n {ORDER.filter((band) => band !== 'texture').map((band, index) => (\n <span key={band}>\n {index === 0 ? '' : '; '}\n <em>{senseOf(band)}</em> over {READS[band]}\n </span>\n ))}\n . The baseline keeps the digests and not the values, so a band names which of them differ and\n not what they became.\n </p>\n );\n}\n"]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * One change, whole — the page a decision is actually made on.
3
+ *
4
+ * A change is a component and the renders it moved in, and until this page
5
+ * existed all of that was a card in a stack of cards on a build page four
6
+ * thousand pixels tall. The card carried a name, a file path, a ratio and a
7
+ * fingerprint, which between them tell a reviewer nothing they can act on: the
8
+ * hash is not a fact about the change, it is the *identity* of a fact, and it was
9
+ * in the third line of the lead.
10
+ *
11
+ * What a reviewer needs, in the order they need it:
12
+ *
13
+ * 1. **What moved**, in renders rather than pixels. One edit in eleven places is
14
+ * a different afternoon from eleven edits in eleven places, and the pixel
15
+ * total cannot tell them apart.
16
+ * 2. **Whether they asked for it.** The commit either arrives at this component
17
+ * or it does not, and *it does not, in a render nothing else reaches either*
18
+ * is the loudest sentence this product can print.
19
+ * 3. **Whether they have seen it.** The run before this one either carried the
20
+ * same difference or did not, and if it did, somebody either decided it or
21
+ * left it. A docket delivered twice to a reviewer who read it once is the
22
+ * failure the crossing exists to name, and it is named *here*, beside the
23
+ * change — not in a panel nine thousand pixels below it.
24
+ * 4. **Whether it is normal.** The same 2px shift is a bug in a component nobody
25
+ * has touched since March and a Tuesday in one that moves in nineteen runs out
26
+ * of twenty.
27
+ * 5. **The picture**, cropped to where the run measured it.
28
+ * 6. **Where it showed up**, one row per render, each decidable on its own.
29
+ *
30
+ * The two buttons are in the head, not at the bottom. Every line under them is a
31
+ * reason to press one or to refuse; a reviewer who has read them should not have
32
+ * to scroll back past the reasons to act on them.
33
+ */
34
+ import { type ReactElement } from 'react';
35
+ import type { BuildDetail } from '../review-types.js';
36
+ import type { ReviewClient } from './client.js';
37
+ import type { Crossing } from './crossing.js';
38
+ import type { Origin } from './grouping.js';
39
+ import type { Route } from './route.js';
40
+ export declare function ChangePanel({ client, reviewer, build, origin, crossing, changes, go, onDecided, }: {
41
+ readonly client: ReviewClient;
42
+ readonly reviewer: string;
43
+ readonly build: BuildDetail;
44
+ readonly origin: Origin;
45
+ readonly crossing: Crossing;
46
+ /** Every component with a change page on this build, so a link goes somewhere. */
47
+ readonly changes: ReadonlySet<string>;
48
+ readonly go: (route: Route) => void;
49
+ readonly onDecided: () => void;
50
+ }): ReactElement;
51
+ //# sourceMappingURL=change.d.ts.map
@@ -0,0 +1,125 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * One change, whole — the page a decision is actually made on.
4
+ *
5
+ * A change is a component and the renders it moved in, and until this page
6
+ * existed all of that was a card in a stack of cards on a build page four
7
+ * thousand pixels tall. The card carried a name, a file path, a ratio and a
8
+ * fingerprint, which between them tell a reviewer nothing they can act on: the
9
+ * hash is not a fact about the change, it is the *identity* of a fact, and it was
10
+ * in the third line of the lead.
11
+ *
12
+ * What a reviewer needs, in the order they need it:
13
+ *
14
+ * 1. **What moved**, in renders rather than pixels. One edit in eleven places is
15
+ * a different afternoon from eleven edits in eleven places, and the pixel
16
+ * total cannot tell them apart.
17
+ * 2. **Whether they asked for it.** The commit either arrives at this component
18
+ * or it does not, and *it does not, in a render nothing else reaches either*
19
+ * is the loudest sentence this product can print.
20
+ * 3. **Whether they have seen it.** The run before this one either carried the
21
+ * same difference or did not, and if it did, somebody either decided it or
22
+ * left it. A docket delivered twice to a reviewer who read it once is the
23
+ * failure the crossing exists to name, and it is named *here*, beside the
24
+ * change — not in a panel nine thousand pixels below it.
25
+ * 4. **Whether it is normal.** The same 2px shift is a bug in a component nobody
26
+ * has touched since March and a Tuesday in one that moves in nineteen runs out
27
+ * of twenty.
28
+ * 5. **The picture**, cropped to where the run measured it.
29
+ * 6. **Where it showed up**, one row per render, each decidable on its own.
30
+ *
31
+ * The two buttons are in the head, not at the bottom. Every line under them is a
32
+ * reason to press one or to refuse; a reviewer who has read them should not have
33
+ * to scroll back past the reasons to act on them.
34
+ */
35
+ import { useState } from 'react';
36
+ import { Arrival, Collateral, Declared, Evidence, Recurrence, Shapes, SinceLast, Tally, } from './change-story.js';
37
+ import { Because } from './because.js';
38
+ import { causesOf, namesDeclaration, UnderRoot } from './root.js';
39
+ import { AlsoCarries, carriedWith } from './carried.js';
40
+ import { Consumers, consumersOf } from './consumers.js';
41
+ import { HeldStill, controlsFor } from './control.js';
42
+ import { distanceFrom } from './distance.js';
43
+ import { handedTo } from './handed.js';
44
+ import { Look } from './look.js';
45
+ import { HandedTo, MovedElsewhere, MovedLead, WhatMoved } from './moved.js';
46
+ import { movedElsewhere, senseAcross } from './sense.js';
47
+ import { Go, messageOf } from './shell.js';
48
+ import { count, number } from './text.js';
49
+ export function ChangePanel({ client, reviewer, build, origin, crossing, changes, go, onDecided, }) {
50
+ const sourced = build.causes.some((cause) => cause.file !== undefined);
51
+ const across = senseAcross(origin.component, origin.appearances);
52
+ const handed = handedTo(build)(origin.component);
53
+ const consumers = consumersOf(build)(origin.component);
54
+ const carrying = carriedWith(build, origin.component, origin.appearances.map(({ subject }) => subject.subject));
55
+ const far = distanceFrom(build, origin.component);
56
+ const elsewhere = movedElsewhere(origin.component, build.subjects, new Set(origin.appearances.map(({ subject }) => subject.subject)));
57
+ const causes = causesOf(build, origin);
58
+ const settled = origin.appearances.filter(({ subject }) => subject.decision !== null).length;
59
+ const open = origin.appearances.filter(({ subject }) => subject.decision === null && subject.approvable);
60
+ return (_jsx("div", { className: "va-stage va-scroll", children: _jsxs("div", { className: "va-page va-decide", children: [_jsxs("header", { className: "va-decide-head", children: [_jsxs("div", { children: [_jsx("h1", { children: origin.component }), _jsx(UnderRoot, { bands: causes, component: origin.component, ...(origin.file === undefined ? {} : { declared: origin.file }), build: build.build, go: go }), namesDeclaration(causes, origin.file) ? null : (_jsx(Declared, { file: origin.file, sourced: sourced }))] }), _jsx(Batch, { client: client, reviewer: reviewer, build: build.build, origin: origin, open: open, settled: settled, onDecided: onDecided })] }), _jsx(MovedLead, { component: origin.component, across: across, handed: handed }), _jsx(Tally, { origin: origin, elsewhere: elsewhere.length, open: open.length }), _jsx(AlsoCarries, { carrying: carrying, renders: origin.appearances.length, build: build.build, changes: changes, go: go }), _jsx(HandedTo, { handed: handed, build: build.build, changes: changes, go: go }), _jsx(Look, { client: client, build: build.build, component: origin.component, appearances: origin.appearances, onOpen: (subject) => go({ page: 'subject', build: build.build, subject }) }), _jsx(Consumers, { found: consumers, build: build.build, changes: changes, go: go }), _jsx(Because, { origin: origin, build: build.build, changes: changes, go: go }), _jsx(HeldStill, { controls: controlsFor(origin), build: build.build, go: go }), _jsx(Arrival, { origin: origin }), _jsx(WhatMoved, { component: origin.component, across: across, far: far, except: new Set(consumers.map((each) => each.component)) }), _jsx(MovedElsewhere, { component: origin.component, found: elsewhere, build: build.build, go: go }), _jsx(SinceLast, { crossing: crossing, origin: origin }), _jsx(Recurrence, { client: client, component: origin.component }), _jsx(Shapes, { origin: origin }), _jsx("h2", { children: "Where it showed up" }), _jsx("ul", { className: "va-where", children: origin.appearances.map((appearance) => (_jsx(Where, { client: client, reviewer: reviewer, build: build.build, appearance: appearance, apart: appearance.movement !== undefined && appearance.movement.cause !== origin.cause, also: carrying.at(appearance.subject.subject), was: crossing.state === 'ready' ? crossing.of(appearance.subject.subject) : undefined, against: crossing.state === 'ready' ? crossing.earlier.build : undefined, go: go, onDecided: onDecided }, appearance.subject.subject))) }), _jsx(Collateral, { build: build }), _jsx(Evidence, {})] }) }));
61
+ }
62
+ /** One render this change showed up in, and the decision on that one render. */
63
+ function Where({ client, reviewer, build, appearance, apart, also, was, against, go, onDecided, }) {
64
+ const { subject, pixels } = appearance;
65
+ // Minus the names the mark beside it already carries. `alongside` is every
66
+ // component the regions put in this render, causes and pushed together, and
67
+ // printing `also CardFooter` next to `alongside CardFooter` spends the row's
68
+ // width saying one name twice under two words that do not mean the same thing.
69
+ const alongside = appearance.alongside.filter((name) => !also.includes(name));
70
+ const [busy, setBusy] = useState(false);
71
+ const [failed, setFailed] = useState(undefined);
72
+ const decide = async (decision) => {
73
+ setBusy(true);
74
+ setFailed(undefined);
75
+ try {
76
+ await client.decide(build, subject.subject, decision, reviewer);
77
+ onDecided();
78
+ }
79
+ catch (error) {
80
+ setFailed(messageOf(error));
81
+ }
82
+ finally {
83
+ setBusy(false);
84
+ }
85
+ };
86
+ return (_jsxs("li", { className: "va-where-row", children: [_jsx(Go, { to: { page: 'subject', build, subject: subject.subject }, go: go, className: "va-where-subject", children: subject.subject }), _jsxs("span", { className: "va-where-size va-num va-note", children: [number(pixels), " px"] }), also.length === 0 ? null : (_jsxs("span", { className: "va-mark va-carries-mark", title: "Approving this render accepts these here too", children: ["also ", also.join(', ')] })), apart && appearance.movement !== undefined ? (_jsx("span", { className: "va-mark va-note", title: appearance.movement.because, children: appearance.movement.upstream === undefined
87
+ ? appearance.movement.cause
88
+ : `from ${appearance.movement.upstream}` })) : null, was === undefined || was.shift !== 'again' ? null : (_jsxs("span", { className: "va-mark va-known", children: ["same as ", against === undefined ? 'the last run' : `build ${against}`, was.earlier?.decision == null ? ', undecided' : ''] })), alongside.length === 0 ? null : (_jsxs("span", { className: "va-where-with va-note", title: alongside.join(', '), children: ["alongside ", alongside.slice(0, 3).join(', '), alongside.length > 3 ? ` and ${count(alongside.length - 3, 'other')}` : ''] })), subject.decision === null ? (_jsxs("span", { className: "va-where-act", children: [_jsx("button", { type: "button", className: "va-approve", disabled: busy || !subject.approvable, onClick: () => void decide('approved'), title: subject.approvable
89
+ ? 'Promote this render’s candidate, and no others'
90
+ : 'This run kept no candidate image for this subject, so there is nothing to promote.', children: "\u2713" }), _jsx("button", { type: "button", disabled: busy, onClick: () => void decide('rejected'), children: "\u2715" })] })) : (_jsxs("span", { className: `va-mark va-${subject.decision.decision}`, children: [subject.decision.decision === 'approved' ? '✓' : '✕', " ", subject.decision.by] })), failed === undefined ? null : _jsx("span", { className: "va-failure", children: failed })] }));
91
+ }
92
+ /**
93
+ * One decision, applied to every render the change showed up in.
94
+ *
95
+ * The store keeps a row per subject and this does not change that: what is shared
96
+ * is the *act*, not the record. A group decision as a stored entity would be a
97
+ * second thing that can disagree with the rows under it, and the note is what
98
+ * carries the reviewer's reason to each one.
99
+ */
100
+ function Batch({ client, reviewer, build, origin, open, settled, onDecided, }) {
101
+ const [busy, setBusy] = useState(false);
102
+ const [failed, setFailed] = useState(undefined);
103
+ const blocked = origin.appearances.length - open.length - settled;
104
+ const decide = async (decision) => {
105
+ setBusy(true);
106
+ setFailed(undefined);
107
+ try {
108
+ for (const { subject } of open) {
109
+ await client.decide(build, subject.subject, decision, reviewer, `${decision} as one change in ${origin.component}`);
110
+ }
111
+ onDecided();
112
+ }
113
+ catch (error) {
114
+ // Reported rather than swallowed, and the ones already written stay
115
+ // written: a batch that rolled itself back would undo decisions a reviewer
116
+ // made, to tidy up a network error.
117
+ setFailed(messageOf(error));
118
+ }
119
+ finally {
120
+ setBusy(false);
121
+ }
122
+ };
123
+ return (_jsxs("div", { className: "va-decide-act", children: [_jsxs("button", { type: "button", className: "va-approve", disabled: busy || open.length === 0, onClick: () => void decide('approved'), title: `Promote the candidate in every render ${origin.component} moved`, children: ["Approve this change", open.length === 0 ? '' : ` (${number(open.length)})`] }), _jsx("button", { type: "button", disabled: busy || open.length === 0, onClick: () => void decide('rejected'), children: "Reject" }), settled === 0 ? null : _jsxs("span", { className: "va-note", children: [number(settled), " already decided"] }), blocked === 0 ? null : (_jsxs("span", { className: "va-note", children: [count(blocked, 'render'), " kept no candidate and cannot be approved here"] })), failed === undefined ? null : _jsx("p", { className: "va-failure", children: failed })] }));
124
+ }
125
+ //# sourceMappingURL=change.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"change.js","sourceRoot":"","sources":["../../src/ui/change.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * One change, whole — the page a decision is actually made on.\n *\n * A change is a component and the renders it moved in, and until this page\n * existed all of that was a card in a stack of cards on a build page four\n * thousand pixels tall. The card carried a name, a file path, a ratio and a\n * fingerprint, which between them tell a reviewer nothing they can act on: the\n * hash is not a fact about the change, it is the *identity* of a fact, and it was\n * in the third line of the lead.\n *\n * What a reviewer needs, in the order they need it:\n *\n * 1. **What moved**, in renders rather than pixels. One edit in eleven places is\n * a different afternoon from eleven edits in eleven places, and the pixel\n * total cannot tell them apart.\n * 2. **Whether they asked for it.** The commit either arrives at this component\n * or it does not, and *it does not, in a render nothing else reaches either*\n * is the loudest sentence this product can print.\n * 3. **Whether they have seen it.** The run before this one either carried the\n * same difference or did not, and if it did, somebody either decided it or\n * left it. A docket delivered twice to a reviewer who read it once is the\n * failure the crossing exists to name, and it is named *here*, beside the\n * change — not in a panel nine thousand pixels below it.\n * 4. **Whether it is normal.** The same 2px shift is a bug in a component nobody\n * has touched since March and a Tuesday in one that moves in nineteen runs out\n * of twenty.\n * 5. **The picture**, cropped to where the run measured it.\n * 6. **Where it showed up**, one row per render, each decidable on its own.\n *\n * The two buttons are in the head, not at the bottom. Every line under them is a\n * reason to press one or to refuse; a reviewer who has read them should not have\n * to scroll back past the reasons to act on them.\n */\n\nimport { useState, type ReactElement } from 'react';\nimport type { BuildDetail, Decision } from '../review-types.js';\nimport {\n Arrival,\n Collateral,\n Declared,\n Evidence,\n Recurrence,\n Shapes,\n SinceLast,\n Tally,\n} from './change-story.js';\nimport { Because } from './because.js';\nimport { causesOf, namesDeclaration, UnderRoot } from './root.js';\nimport { AlsoCarries, carriedWith } from './carried.js';\nimport type { ReviewClient } from './client.js';\nimport { Consumers, consumersOf } from './consumers.js';\nimport { HeldStill, controlsFor } from './control.js';\nimport type { Crossing } from './crossing.js';\nimport { distanceFrom } from './distance.js';\nimport type { Appearance, Origin } from './grouping.js';\nimport { handedTo } from './handed.js';\nimport { Look } from './look.js';\nimport { HandedTo, MovedElsewhere, MovedLead, WhatMoved } from './moved.js';\nimport { movedElsewhere, senseAcross } from './sense.js';\nimport { Go, messageOf } from './shell.js';\nimport type { Route } from './route.js';\nimport type { Shifted } from './shift.js';\nimport { count, number } from './text.js';\n\nexport function ChangePanel({\n client,\n reviewer,\n build,\n origin,\n crossing,\n changes,\n go,\n onDecided,\n}: {\n readonly client: ReviewClient;\n readonly reviewer: string;\n readonly build: BuildDetail;\n readonly origin: Origin;\n readonly crossing: Crossing;\n /** Every component with a change page on this build, so a link goes somewhere. */\n readonly changes: ReadonlySet<string>;\n readonly go: (route: Route) => void;\n readonly onDecided: () => void;\n}): ReactElement {\n const sourced = build.causes.some((cause) => cause.file !== undefined);\n const across = senseAcross(origin.component, origin.appearances);\n const handed = handedTo(build)(origin.component);\n const consumers = consumersOf(build)(origin.component);\n const carrying = carriedWith(\n build,\n origin.component,\n origin.appearances.map(({ subject }) => subject.subject),\n );\n const far = distanceFrom(build, origin.component);\n const elsewhere = movedElsewhere(\n origin.component,\n build.subjects,\n new Set(origin.appearances.map(({ subject }) => subject.subject)),\n );\n const causes = causesOf(build, origin);\n const settled = origin.appearances.filter(({ subject }) => subject.decision !== null).length;\n const open = origin.appearances.filter(\n ({ subject }) => subject.decision === null && subject.approvable,\n );\n\n return (\n <div className=\"va-stage va-scroll\">\n <div className=\"va-page va-decide\">\n <header className=\"va-decide-head\">\n <div>\n <h1>{origin.component}</h1>\n {/* What in the commit this hangs from, above where it is declared.\n A reviewer arriving here asks *why is this on my docket* before\n *where does this live*, and the answer to the first is a file\n they edited while the answer to the second usually is not. */}\n <UnderRoot\n bands={causes}\n component={origin.component}\n {...(origin.file === undefined ? {} : { declared: origin.file })}\n build={build.build}\n go={go}\n />\n {/* Only when the cause line above is not already carrying it. */}\n {namesDeclaration(causes, origin.file) ? null : (\n <Declared file={origin.file} sourced={sourced} />\n )}\n </div>\n <Batch\n client={client}\n reviewer={reviewer}\n build={build.build}\n origin={origin}\n open={open}\n settled={settled}\n onDecided={onDecided}\n />\n </header>\n\n <MovedLead component={origin.component} across={across} handed={handed} />\n\n {/* The three counts, split. They answer three different questions and\n they are three different numbers. */}\n <Tally origin={origin} elsewhere={elsewhere.length} open={open.length} />\n\n {/* What a press of the button up there settles that is not this change.\n A decision is taken on the whole picture. */}\n <AlsoCarries\n carrying={carrying}\n renders={origin.appearances.length}\n build={build.build}\n changes={changes}\n go={go}\n />\n\n {/* What the lead just stopped saying, and who owns it. A band a parent\n hands down is decided in the parent's file, so it is named before the\n reviewer looks at a crop and goes hunting for it in this one. */}\n <HandedTo handed={handed} build={build.build} changes={changes} go={go} />\n\n {/* Second, and before a word of prose: the differences themselves, one\n per shape. Everything under this is the page reasoning about a change\n the reviewer can now see, and a decision made without seeing it is the\n failure this whole surface exists to prevent. */}\n <Look\n client={client}\n build={build.build}\n component={origin.component}\n appearances={origin.appearances}\n onOpen={(subject) => go({ page: 'subject', build: build.build, subject })}\n />\n\n {/* Then the consequence. Editing a leaf and being told the leaf moved is\n not news — the reviewer wrote it. What they cannot know without being\n told is what happened to everything that draws it. */}\n <Consumers found={consumers} build={build.build} changes={changes} go={go} />\n\n {/* Then why. A reviewer who has just read *what* moved and looked at it\n asks one question, and the two lines that answer it belong under the\n question rather than three sections down past the collateral. */}\n <Because origin={origin} build={build.build} changes={changes} go={go} />\n\n {/* And the other arm. The sentence above names what explains the change;\n this names where the same component under the same inputs was read and\n did not take it, which is the only line on the page derived from\n renders that did not move. */}\n <HeldStill controls={controlsFor(origin)} build={build.build} go={go} />\n <Arrival origin={origin} />\n\n <WhatMoved\n component={origin.component}\n across={across}\n far={far}\n except={new Set(consumers.map((each) => each.component))}\n />\n <MovedElsewhere\n component={origin.component}\n found={elsewhere}\n build={build.build}\n go={go}\n />\n <SinceLast crossing={crossing} origin={origin} />\n <Recurrence client={client} component={origin.component} />\n <Shapes origin={origin} />\n\n <h2>Where it showed up</h2>\n <ul className=\"va-where\">\n {origin.appearances.map((appearance) => (\n <Where\n key={appearance.subject.subject}\n client={client}\n reviewer={reviewer}\n build={build.build}\n appearance={appearance}\n apart={\n appearance.movement !== undefined && appearance.movement.cause !== origin.cause\n }\n also={carrying.at(appearance.subject.subject)}\n was={crossing.state === 'ready' ? crossing.of(appearance.subject.subject) : undefined}\n against={crossing.state === 'ready' ? crossing.earlier.build : undefined}\n go={go}\n onDecided={onDecided}\n />\n ))}\n </ul>\n\n <Collateral build={build} />\n <Evidence />\n </div>\n </div>\n );\n}\n\n/** One render this change showed up in, and the decision on that one render. */\nfunction Where({\n client,\n reviewer,\n build,\n appearance,\n apart,\n also,\n was,\n against,\n go,\n onDecided,\n}: {\n readonly client: ReviewClient;\n readonly reviewer: string;\n readonly build: string;\n readonly appearance: Appearance;\n /** This render moved for a different reason than the change as a whole. */\n readonly apart: boolean;\n /** Other components that moved on their own here, which a press also settles. */\n readonly also: readonly string[];\n readonly was?: Shifted | undefined;\n readonly against?: string | undefined;\n readonly go: (route: Route) => void;\n readonly onDecided: () => void;\n}): ReactElement {\n const { subject, pixels } = appearance;\n // Minus the names the mark beside it already carries. `alongside` is every\n // component the regions put in this render, causes and pushed together, and\n // printing `also CardFooter` next to `alongside CardFooter` spends the row's\n // width saying one name twice under two words that do not mean the same thing.\n const alongside = appearance.alongside.filter((name) => !also.includes(name));\n const [busy, setBusy] = useState(false);\n const [failed, setFailed] = useState<string | undefined>(undefined);\n\n const decide = async (decision: Decision): Promise<void> => {\n setBusy(true);\n setFailed(undefined);\n try {\n await client.decide(build, subject.subject, decision, reviewer);\n onDecided();\n } catch (error) {\n setFailed(messageOf(error));\n } finally {\n setBusy(false);\n }\n };\n\n return (\n <li className=\"va-where-row\">\n <Go\n to={{ page: 'subject', build, subject: subject.subject }}\n go={go}\n className=\"va-where-subject\"\n >\n {subject.subject}\n </Go>\n <span className=\"va-where-size va-num va-note\">{number(pixels)} px</span>\n {/* Approving this row promotes the whole picture, and these moved in it on\n their own. The row is where the single ✓ is, so the names belong here\n and not only in the summary above. */}\n {also.length === 0 ? null : (\n <span className=\"va-mark va-carries-mark\" title=\"Approving this render accepts these here too\">\n also {also.join(', ')}\n </span>\n )}\n {apart && appearance.movement !== undefined ? (\n <span className=\"va-mark va-note\" title={appearance.movement.because}>\n {appearance.movement.upstream === undefined\n ? appearance.movement.cause\n : `from ${appearance.movement.upstream}`}\n </span>\n ) : null}\n {was === undefined || was.shift !== 'again' ? null : (\n <span className=\"va-mark va-known\">\n same as {against === undefined ? 'the last run' : `build ${against}`}\n {was.earlier?.decision == null ? ', undecided' : ''}\n </span>\n )}\n {alongside.length === 0 ? null : (\n <span className=\"va-where-with va-note\" title={alongside.join(', ')}>\n alongside {alongside.slice(0, 3).join(', ')}\n {alongside.length > 3 ? ` and ${count(alongside.length - 3, 'other')}` : ''}\n </span>\n )}\n {subject.decision === null ? (\n <span className=\"va-where-act\">\n <button\n type=\"button\"\n className=\"va-approve\"\n disabled={busy || !subject.approvable}\n onClick={() => void decide('approved')}\n title={\n subject.approvable\n ? 'Promote this render’s candidate, and no others'\n : 'This run kept no candidate image for this subject, so there is nothing to promote.'\n }\n >\n ✓\n </button>\n <button type=\"button\" disabled={busy} onClick={() => void decide('rejected')}>\n ✕\n </button>\n </span>\n ) : (\n <span className={`va-mark va-${subject.decision.decision}`}>\n {subject.decision.decision === 'approved' ? '✓' : '✕'} {subject.decision.by}\n </span>\n )}\n {failed === undefined ? null : <span className=\"va-failure\">{failed}</span>}\n </li>\n );\n}\n\n/**\n * One decision, applied to every render the change showed up in.\n *\n * The store keeps a row per subject and this does not change that: what is shared\n * is the *act*, not the record. A group decision as a stored entity would be a\n * second thing that can disagree with the rows under it, and the note is what\n * carries the reviewer's reason to each one.\n */\nfunction Batch({\n client,\n reviewer,\n build,\n origin,\n open,\n settled,\n onDecided,\n}: {\n readonly client: ReviewClient;\n readonly reviewer: string;\n readonly build: string;\n readonly origin: Origin;\n readonly open: readonly Appearance[];\n readonly settled: number;\n readonly onDecided: () => void;\n}): ReactElement {\n const [busy, setBusy] = useState(false);\n const [failed, setFailed] = useState<string | undefined>(undefined);\n\n const blocked = origin.appearances.length - open.length - settled;\n\n const decide = async (decision: Decision): Promise<void> => {\n setBusy(true);\n setFailed(undefined);\n try {\n for (const { subject } of open) {\n await client.decide(\n build,\n subject.subject,\n decision,\n reviewer,\n `${decision} as one change in ${origin.component}`,\n );\n }\n onDecided();\n } catch (error) {\n // Reported rather than swallowed, and the ones already written stay\n // written: a batch that rolled itself back would undo decisions a reviewer\n // made, to tidy up a network error.\n setFailed(messageOf(error));\n } finally {\n setBusy(false);\n }\n };\n\n return (\n <div className=\"va-decide-act\">\n <button\n type=\"button\"\n className=\"va-approve\"\n disabled={busy || open.length === 0}\n onClick={() => void decide('approved')}\n title={`Promote the candidate in every render ${origin.component} moved`}\n >\n Approve this change{open.length === 0 ? '' : ` (${number(open.length)})`}\n </button>\n <button\n type=\"button\"\n disabled={busy || open.length === 0}\n onClick={() => void decide('rejected')}\n >\n Reject\n </button>\n {settled === 0 ? null : <span className=\"va-note\">{number(settled)} already decided</span>}\n {blocked === 0 ? null : (\n <span className=\"va-note\">\n {count(blocked, 'render')} kept no candidate and cannot be approved here\n </span>\n )}\n {failed === undefined ? null : <p className=\"va-failure\">{failed}</p>}\n </div>\n );\n}\n"]}
@@ -0,0 +1,74 @@
1
+ import { type Churn, type Flakiness, type Observation, type Reach, type Window } from '@variance-authority/history';
2
+ import type { BuildDetail, BuildSummary, Decision, DecisionRecord, SweepReport, TribunalChangelog, TribunalChangelogQuery } from '../review.js';
3
+ /**
4
+ * The review API, as the browser sees it.
5
+ *
6
+ * Separate from the components so the surface can be driven by something that is
7
+ * not React — a script, a terminal, another team's dashboard — and so the
8
+ * components can be rendered against a fake without a server. Everything it
9
+ * returns is the same shape `review.ts` produced, because the Worker serializes
10
+ * those types directly rather than reshaping them for a viewer.
11
+ *
12
+ * ## Every non-2xx throws
13
+ *
14
+ * The same rule the baseline and history clients follow, for a weaker but real
15
+ * version of the same reason. A list that answered `[]` to a 500 says "no builds
16
+ * need review" — which is the sentence somebody merges on.
17
+ */
18
+ export interface ReviewClientOptions {
19
+ /**
20
+ * Where the Worker is mounted, e.g. `https://variance.example.com` or `/api`.
21
+ *
22
+ * A relative base is the normal case behind the Next.js adapter, where the
23
+ * page and the API are the same deployment and the browser has the session
24
+ * anyway.
25
+ */
26
+ readonly endpoint: string;
27
+ /**
28
+ * The review token, when the caller holds it directly.
29
+ *
30
+ * Omit it behind the Next.js adapter: there the server route holds the token
31
+ * and the browser never sees it, which is the entire reason that adapter exists.
32
+ * A review token shipped to a browser is a token in everybody's devtools.
33
+ */
34
+ readonly token?: string;
35
+ readonly fetch?: typeof globalThis.fetch;
36
+ }
37
+ export interface ReviewClient {
38
+ builds(limit?: number): Promise<readonly BuildSummary[]>;
39
+ build(id: string): Promise<BuildDetail>;
40
+ /** Why the baselines are what they are, grouped by shape. */
41
+ changelog(query?: TribunalChangelogQuery): Promise<TribunalChangelog>;
42
+ /**
43
+ * The three history answers a reviewer needs to place one difference in time.
44
+ *
45
+ * They read the same rows `variance` writes and are served on the same paths a
46
+ * CLI addresses — this deployment is one service, and the review page is a
47
+ * second reader of the record rather than a second record. Never `Unkept`: a
48
+ * tribunal that is answering at all has a database, and the sentence about
49
+ * nobody keeping a record belongs to a pipeline with no service configured.
50
+ */
51
+ churn(component: string, window?: Window): Promise<Churn>;
52
+ reach(component: string, window?: Window): Promise<Reach>;
53
+ flakiness(subject: string, window?: Window): Promise<Flakiness>;
54
+ /** The most recent recorded reading of one component in one subject, or `null`. */
55
+ lastChanged(subject: string, component: string): Promise<Observation | null>;
56
+ decide(build: string, subject: string, decision: Decision, by: string, note?: string): Promise<DecisionRecord>;
57
+ sweep(days?: number): Promise<SweepReport>;
58
+ /** The URL of one image, for an `<img src>`. Never fetched here. */
59
+ imageUrl(build: string, subject: string, kind: 'before' | 'after' | 'diff'): string;
60
+ }
61
+ export declare class ReviewRequestError extends Error {
62
+ readonly status: number;
63
+ readonly name = "ReviewRequestError";
64
+ constructor(message: string, status: number);
65
+ }
66
+ /**
67
+ * A review client over `fetch`, pointed at wherever the service is mounted.
68
+ *
69
+ * The one dependency {@link ReviewApp} has. Everything non-2xx throws
70
+ * {@link ReviewRequestError} with the status, so a failure reaches the surface
71
+ * as a failure rather than as an empty list somebody merges on.
72
+ */
73
+ export declare function createReviewClient(options: ReviewClientOptions): ReviewClient;
74
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,88 @@
1
+ import { CHURN_PATH, FLAKINESS_PATH, LAST_CHANGED_PATH, REACH_PATH, } from '@variance-authority/history';
2
+ export class ReviewRequestError extends Error {
3
+ status;
4
+ name = 'ReviewRequestError';
5
+ constructor(message, status) {
6
+ super(message);
7
+ this.status = status;
8
+ }
9
+ }
10
+ /**
11
+ * A review client over `fetch`, pointed at wherever the service is mounted.
12
+ *
13
+ * The one dependency {@link ReviewApp} has. Everything non-2xx throws
14
+ * {@link ReviewRequestError} with the status, so a failure reaches the surface
15
+ * as a failure rather than as an empty list somebody merges on.
16
+ */
17
+ export function createReviewClient(options) {
18
+ const base = options.endpoint.replace(/\/$/, '');
19
+ const send = options.fetch ?? globalThis.fetch.bind(globalThis);
20
+ async function call(path, init) {
21
+ const headers = { 'content-type': 'application/json' };
22
+ if (options.token !== undefined)
23
+ headers['authorization'] = `Bearer ${options.token}`;
24
+ const response = await send(`${base}${path}`, { ...init, headers });
25
+ if (!response.ok) {
26
+ throw new ReviewRequestError(`${init?.method ?? 'GET'} ${path} answered ${response.status}: ${await quote(response)}`, response.status);
27
+ }
28
+ return (await response.json());
29
+ }
30
+ const encode = (value) => encodeURIComponent(value);
31
+ return {
32
+ async builds(limit) {
33
+ const body = await call(`/review/builds${limit === undefined ? '' : `?limit=${limit}`}`);
34
+ return body.builds;
35
+ },
36
+ build: (id) => call(`/review/builds/${encode(id)}`),
37
+ changelog: (query = {}) => call(`/review/changelog${search({
38
+ component: query.component,
39
+ subject: query.subject,
40
+ since: query.since,
41
+ limit: query.limit,
42
+ })}`),
43
+ churn: (component, window) => call(`${CHURN_PATH}${search({ component, ...window })}`),
44
+ reach: (component, window) => call(`${REACH_PATH}${search({ component, ...window })}`),
45
+ flakiness: (subject, window) => call(`${FLAKINESS_PATH}${search({ subject, ...window })}`),
46
+ async lastChanged(subject, component) {
47
+ const body = await call(`${LAST_CHANGED_PATH}${search({ subject, component })}`);
48
+ return body.observation;
49
+ },
50
+ decide: (build, subject, decision, by, note) => call(`/review/builds/${encode(build)}/subjects/${encode(subject)}/decision`, {
51
+ method: 'POST',
52
+ body: JSON.stringify({ decision, by, ...(note === undefined ? {} : { note }) }),
53
+ }),
54
+ sweep: (days) => call(`/review/sweep${days === undefined ? '' : `?days=${days}`}`, {
55
+ method: 'POST',
56
+ }),
57
+ imageUrl: (build, subject, kind) => `${base}/review/builds/${encode(build)}/subjects/${encode(subject)}/${kind}.png`,
58
+ };
59
+ }
60
+ /**
61
+ * A query string from the parameters that were actually given.
62
+ *
63
+ * An absent filter is left out rather than sent empty, because on these routes
64
+ * the two mean different things: `component=` is a component whose name is the
65
+ * empty string and matches nothing, while no `component` at all is the whole
66
+ * project. A filter that silently narrows an answer to nothing is the same defect
67
+ * as an empty list returned for a failure.
68
+ */
69
+ function search(parameters) {
70
+ const query = new URLSearchParams();
71
+ for (const [key, value] of Object.entries(parameters)) {
72
+ if (value !== undefined)
73
+ query.set(key, String(value));
74
+ }
75
+ const text = query.toString();
76
+ return text === '' ? '' : `?${text}`;
77
+ }
78
+ /** The server's own sentence, quoted, because it is the one worth reading. */
79
+ async function quote(response) {
80
+ try {
81
+ const text = await response.text();
82
+ return text.trim() === '' ? '<no body>' : text.slice(0, 500);
83
+ }
84
+ catch {
85
+ return '<body could not be read>';
86
+ }
87
+ }
88
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/ui/client.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n CHURN_PATH,\n FLAKINESS_PATH,\n LAST_CHANGED_PATH,\n REACH_PATH,\n type Churn,\n type Flakiness,\n type Observation,\n type Reach,\n type Window,\n} from '@variance-authority/history';\nimport type {\n BuildDetail,\n BuildSummary,\n Decision,\n DecisionRecord,\n SweepReport,\n TribunalChangelog,\n TribunalChangelogQuery,\n} from '../review.js';\n\n/**\n * The review API, as the browser sees it.\n *\n * Separate from the components so the surface can be driven by something that is\n * not React — a script, a terminal, another team's dashboard — and so the\n * components can be rendered against a fake without a server. Everything it\n * returns is the same shape `review.ts` produced, because the Worker serializes\n * those types directly rather than reshaping them for a viewer.\n *\n * ## Every non-2xx throws\n *\n * The same rule the baseline and history clients follow, for a weaker but real\n * version of the same reason. A list that answered `[]` to a 500 says \"no builds\n * need review\" — which is the sentence somebody merges on.\n */\n\nexport interface ReviewClientOptions {\n /**\n * Where the Worker is mounted, e.g. `https://variance.example.com` or `/api`.\n *\n * A relative base is the normal case behind the Next.js adapter, where the\n * page and the API are the same deployment and the browser has the session\n * anyway.\n */\n readonly endpoint: string;\n /**\n * The review token, when the caller holds it directly.\n *\n * Omit it behind the Next.js adapter: there the server route holds the token\n * and the browser never sees it, which is the entire reason that adapter exists.\n * A review token shipped to a browser is a token in everybody's devtools.\n */\n readonly token?: string;\n readonly fetch?: typeof globalThis.fetch;\n}\n\nexport interface ReviewClient {\n builds(limit?: number): Promise<readonly BuildSummary[]>;\n build(id: string): Promise<BuildDetail>;\n /** Why the baselines are what they are, grouped by shape. */\n changelog(query?: TribunalChangelogQuery): Promise<TribunalChangelog>;\n /**\n * The three history answers a reviewer needs to place one difference in time.\n *\n * They read the same rows `variance` writes and are served on the same paths a\n * CLI addresses — this deployment is one service, and the review page is a\n * second reader of the record rather than a second record. Never `Unkept`: a\n * tribunal that is answering at all has a database, and the sentence about\n * nobody keeping a record belongs to a pipeline with no service configured.\n */\n churn(component: string, window?: Window): Promise<Churn>;\n reach(component: string, window?: Window): Promise<Reach>;\n flakiness(subject: string, window?: Window): Promise<Flakiness>;\n /** The most recent recorded reading of one component in one subject, or `null`. */\n lastChanged(subject: string, component: string): Promise<Observation | null>;\n decide(\n build: string,\n subject: string,\n decision: Decision,\n by: string,\n note?: string,\n ): Promise<DecisionRecord>;\n sweep(days?: number): Promise<SweepReport>;\n /** The URL of one image, for an `<img src>`. Never fetched here. */\n imageUrl(build: string, subject: string, kind: 'before' | 'after' | 'diff'): string;\n}\n\nexport class ReviewRequestError extends Error {\n override readonly name = 'ReviewRequestError';\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n }\n}\n\n/**\n * A review client over `fetch`, pointed at wherever the service is mounted.\n *\n * The one dependency {@link ReviewApp} has. Everything non-2xx throws\n * {@link ReviewRequestError} with the status, so a failure reaches the surface\n * as a failure rather than as an empty list somebody merges on.\n */\nexport function createReviewClient(options: ReviewClientOptions): ReviewClient {\n const base = options.endpoint.replace(/\\/$/, '');\n const send = options.fetch ?? globalThis.fetch.bind(globalThis);\n\n async function call<T>(path: string, init?: RequestInit): Promise<T> {\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n if (options.token !== undefined) headers['authorization'] = `Bearer ${options.token}`;\n\n const response = await send(`${base}${path}`, { ...init, headers });\n if (!response.ok) {\n throw new ReviewRequestError(\n `${init?.method ?? 'GET'} ${path} answered ${response.status}: ${await quote(response)}`,\n response.status,\n );\n }\n return (await response.json()) as T;\n }\n\n const encode = (value: string): string => encodeURIComponent(value);\n\n return {\n async builds(limit): Promise<readonly BuildSummary[]> {\n const body = await call<{ readonly builds: readonly BuildSummary[] }>(\n `/review/builds${limit === undefined ? '' : `?limit=${limit}`}`,\n );\n return body.builds;\n },\n\n build: (id) => call<BuildDetail>(`/review/builds/${encode(id)}`),\n\n changelog: (query = {}) =>\n call<TribunalChangelog>(\n `/review/changelog${search({\n component: query.component,\n subject: query.subject,\n since: query.since,\n limit: query.limit,\n })}`,\n ),\n\n churn: (component, window) =>\n call<Churn>(`${CHURN_PATH}${search({ component, ...window })}`),\n\n reach: (component, window) =>\n call<Reach>(`${REACH_PATH}${search({ component, ...window })}`),\n\n flakiness: (subject, window) =>\n call<Flakiness>(`${FLAKINESS_PATH}${search({ subject, ...window })}`),\n\n async lastChanged(subject, component): Promise<Observation | null> {\n const body = await call<{ readonly observation: Observation | null }>(\n `${LAST_CHANGED_PATH}${search({ subject, component })}`,\n );\n return body.observation;\n },\n\n decide: (build, subject, decision, by, note) =>\n call<DecisionRecord>(`/review/builds/${encode(build)}/subjects/${encode(subject)}/decision`, {\n method: 'POST',\n body: JSON.stringify({ decision, by, ...(note === undefined ? {} : { note }) }),\n }),\n\n sweep: (days) =>\n call<SweepReport>(`/review/sweep${days === undefined ? '' : `?days=${days}`}`, {\n method: 'POST',\n }),\n\n imageUrl: (build, subject, kind) =>\n `${base}/review/builds/${encode(build)}/subjects/${encode(subject)}/${kind}.png`,\n };\n}\n\n/**\n * A query string from the parameters that were actually given.\n *\n * An absent filter is left out rather than sent empty, because on these routes\n * the two mean different things: `component=` is a component whose name is the\n * empty string and matches nothing, while no `component` at all is the whole\n * project. A filter that silently narrows an answer to nothing is the same defect\n * as an empty list returned for a failure.\n */\nfunction search(parameters: Record<string, string | number | undefined>): string {\n const query = new URLSearchParams();\n for (const [key, value] of Object.entries(parameters)) {\n if (value !== undefined) query.set(key, String(value));\n }\n const text = query.toString();\n return text === '' ? '' : `?${text}`;\n}\n\n/** The server's own sentence, quoted, because it is the one worth reading. */\nasync function quote(response: Response): Promise<string> {\n try {\n const text = await response.text();\n return text.trim() === '' ? '<no body>' : text.slice(0, 500);\n } catch {\n return '<body could not be read>';\n }\n}\n"]}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * What happened to the things that draw this one.
3
+ *
4
+ * A reviewer who edited `ui/button.tsx` does not need to be told the button
5
+ * changed. They wrote it. The question the screenshot exists to answer is the
6
+ * other one — *and does everything that uses it still look right* — and the page
7
+ * answered it by listing every component that moved anywhere near, alphabetised,
8
+ * with no way to tell a card that reflowed from a nav that did something of its
9
+ * own.
10
+ *
11
+ * The census names the consumers directly: `within` is what encloses this
12
+ * component, per render, over the suite. Held to the renders where both are
13
+ * drawn, that turns into one row per consumer and a state for each.
14
+ *
15
+ * ## Four states, and `cause` is what separates the middle two
16
+ *
17
+ * A component hash carries `cause` beside its bands — *its own content moved*,
18
+ * as against *only its rect did*. That distinction is the whole answer here. A
19
+ * card whose row grew because the button inside it got taller was **pushed**: it
20
+ * is behaving correctly, it is the consequence a reviewer is checking for, and
21
+ * printing it as a change would put five components on the alarm list for one
22
+ * edit. A card that moved on its own did something the button does not explain.
23
+ *
24
+ * So: **held still** (no entry at all), **pushed** (an entry, `cause` false),
25
+ * **moved** (an entry, `cause` true, and the bands say in what), and **not read**
26
+ * — a render whose baseline carried no hashes, which is not a render where
27
+ * anything held still.
28
+ *
29
+ * ## What it will not say
30
+ *
31
+ * That a consumer nobody rendered is fine. It says nothing about it, because
32
+ * nothing looked: the rows here are renders, and a consumer with no story and no
33
+ * route has none. The import graph knows those names and this does not read it.
34
+ */
35
+ import type { ReactElement } from 'react';
36
+ import type { BuildDetail } from '../review-types.js';
37
+ import type { Route } from './route.js';
38
+ /** What one component that draws this one did, over the renders they share. */
39
+ export interface Consumer {
40
+ readonly component: string;
41
+ /** Renders drawing both, where the hashes were read. */
42
+ readonly renders: number;
43
+ /** Of those: no entry at all — nothing of it differs. */
44
+ readonly still: number;
45
+ /** Of those: only its rect moved. The consequence, behaving as it should. */
46
+ readonly pushed: number;
47
+ /** Of those: its own content moved, which this change does not explain. */
48
+ readonly moved: number;
49
+ /** The bands it moved in on its own, loudest first. Empty unless `moved`. */
50
+ readonly bands: readonly string[];
51
+ /** Renders drawing both where nothing compared hashes. */
52
+ readonly unread: number;
53
+ }
54
+ /** One build's answer to *what did the things that draw this do*. */
55
+ export type Consuming = (component: string) => readonly Consumer[];
56
+ export declare function consumersOf(build: BuildDetail): Consuming;
57
+ /**
58
+ * What the things drawing this one did — the exceptions named, the rest counted.
59
+ *
60
+ * The first draft gave every consumer a row and led with *2 held still, 2 pushed,
61
+ * 1 moved on its own*, which is three numbers and no names: the reviewer's
62
+ * question is **which**, and a count answers it only if they then read five rows
63
+ * and sort them back into three piles themselves. It also spent a row each on
64
+ * consumers that did nothing, which is the pile with the least to say and, at any
65
+ * real scale, the largest.
66
+ *
67
+ * So the shape is inverted. A consumer that moved in a way this change does not
68
+ * explain gets a row, its bands and its render count — that is the review. The
69
+ * ones that only shifted and the ones that held still are named on one line each
70
+ * and nothing more, because *`MainNav` was drawn eight times and is unchanged* is
71
+ * a fact worth having and not a fact worth eight lines of screen.
72
+ */
73
+ export declare function Consumers({ found, build, changes, go, }: {
74
+ readonly found: readonly Consumer[];
75
+ readonly build: string;
76
+ /** Components with a change page of their own, so a link goes somewhere. */
77
+ readonly changes: ReadonlySet<string>;
78
+ readonly go: (route: Route) => void;
79
+ }): ReactElement | null;
80
+ //# sourceMappingURL=consumers.d.ts.map