@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,414 @@
1
+ import type { AccessibilitySnapshot, RenderIdentity } from '@variance-authority/core';
2
+ import type { FindingRecord, IgnoreLedger, NotObserved, ObservationRecord, ReachHole, ReachedComponent, RegionRecord, RunReport, SensitivityLedger, SubjectReach, VariationRecord } from '@variance-authority/report';
3
+ import type { TribunalBindings } from './bindings.js';
4
+ /**
5
+ * What a review store takes and what it answers with — and nothing that runs.
6
+ *
7
+ * The vocabulary rather than the implementation, apart from it because three
8
+ * other module graphs need the words without the machinery: `worker-input.ts`
9
+ * types the request bodies it validates against these, `ui/client.ts` types its
10
+ * responses against them, and the React surface renders them. None of those
11
+ * should have to name a D1 binding to say what a `BuildSummary` is.
12
+ *
13
+ * The arguments that produced these shapes are on the shapes, because that is
14
+ * where somebody about to change one will look. Why the store *behaves* as it
15
+ * does is in [`review.ts`](./review.ts).
16
+ */
17
+ export interface ReviewOptions extends TribunalBindings {
18
+ readonly project: string;
19
+ /** Injected so tests can pin every `at`. Defaults to the wall clock. */
20
+ readonly now?: () => Date;
21
+ }
22
+ /** One image the run kept, as it arrives. */
23
+ export interface CandidateImage {
24
+ /** Base64 PNG — the same encoding a `Raster` carries, for the same reason. */
25
+ readonly bytes: string;
26
+ }
27
+ /**
28
+ * What a run produced for one subject, over and above the report's record of it.
29
+ *
30
+ * `after` is the candidate and is the only one that can be promoted, which is why
31
+ * it alone carries the sidecar fields. `before` and `diff` exist to be looked at.
32
+ */
33
+ export interface SubjectImages {
34
+ readonly after?: CandidateImage & {
35
+ readonly documentDigest: string;
36
+ readonly width: number;
37
+ readonly height: number;
38
+ readonly missingFonts: readonly string[];
39
+ readonly accessibility?: AccessibilitySnapshot;
40
+ };
41
+ /**
42
+ * The baseline this run compared against, with its own dimensions when they
43
+ * could be read.
44
+ *
45
+ * Optional, and absent means *not measured* — never *the same size as the
46
+ * candidate*. That assumption is the thing this field exists to stop: a
47
+ * baseline drawn to the candidate's box is a width change resampled out of
48
+ * existence, on the one screen where somebody decides whether it is allowed.
49
+ */
50
+ readonly before?: CandidateImage & {
51
+ readonly width?: number;
52
+ readonly height?: number;
53
+ };
54
+ readonly diff?: CandidateImage;
55
+ }
56
+ export interface BuildIngest {
57
+ /** The operator's own id for the run — a CI job number, a workflow run id. */
58
+ readonly build: string;
59
+ readonly commit: string;
60
+ readonly branch?: string;
61
+ readonly report: RunReport;
62
+ /** Keyed by subject. A subject with no entry is recorded with no images. */
63
+ readonly images?: Readonly<Record<string, SubjectImages>>;
64
+ }
65
+ import type { TribunalChangelog, TribunalChangelogQuery } from './changelog.js';
66
+ export type Decision = 'approved' | 'rejected';
67
+ export interface DecisionRecord {
68
+ readonly decision: Decision;
69
+ readonly by: string;
70
+ readonly note?: string;
71
+ readonly at: string;
72
+ }
73
+ export interface BuildSummary {
74
+ readonly project: string;
75
+ readonly build: string;
76
+ readonly commit: string;
77
+ readonly branch?: string;
78
+ readonly intent?: string;
79
+ readonly at: string;
80
+ readonly identity: RenderIdentity;
81
+ readonly retention: 'durable' | 'ephemeral';
82
+ /** One entry per verdict the report used, including the ones with no subjects. */
83
+ readonly verdicts: Readonly<Record<ObservationRecord['verdict'], number>>;
84
+ readonly decided: number;
85
+ /** Subjects whose verdict needs review and that nobody has decided yet. */
86
+ readonly pending: number;
87
+ readonly coverage: Coverage;
88
+ }
89
+ /**
90
+ * What the run said about the subjects it did not observe.
91
+ *
92
+ * `stated: false` is the whole reason this is a shape rather than two numbers. A
93
+ * summary that showed `failed: 0` for a report that never carried the list would
94
+ * be asserting coverage on the authority of a writer that declined to claim any.
95
+ */
96
+ export interface Coverage {
97
+ readonly stated: boolean;
98
+ readonly failed: number;
99
+ readonly excluded: number;
100
+ /** Subjects the run proved this change cannot reach, so never rendered. */
101
+ readonly unreached: number;
102
+ }
103
+ export interface SubjectView {
104
+ readonly subject: string;
105
+ readonly verdict: ObservationRecord['verdict'];
106
+ readonly because: string;
107
+ readonly changedPixels: number;
108
+ readonly regions: readonly RegionRecord[];
109
+ readonly truncated?: {
110
+ readonly regions: number;
111
+ readonly pixels: number;
112
+ };
113
+ readonly missingFonts?: readonly string[];
114
+ readonly findings?: readonly FindingRecord[];
115
+ readonly signals?: ObservationRecord['signals'];
116
+ /**
117
+ * What the operator's ignores took out of this comparison, as the run wrote it.
118
+ *
119
+ * Carried per subject even though the build also carries a ledger, because the
120
+ * two answer different questions. The ledger says a rule absorbed 647 pixels
121
+ * somewhere; this says it absorbed them *here*. A settled list built from the
122
+ * ledger alone can only say a declaration decided this subject, which is the
123
+ * sentence a reviewer already read in the verdict.
124
+ */
125
+ readonly ignored?: ObservationRecord['ignored'];
126
+ /** The sensitivity that decided this subject, when one did. */
127
+ readonly relaxed?: ObservationRecord['relaxed'];
128
+ /**
129
+ * Which components moved here per their hashes, and in which bands.
130
+ *
131
+ * The half of the record {@link SubjectView.regions} cannot carry. A region is
132
+ * named from where its box landed, so an edit that reflowed its neighbours
133
+ * arrives as one blob attributed to the document root; these entries compared
134
+ * digests and never saw a pixel, so they still hold the component and the
135
+ * sense. Absent means the baseline carried no hashes — never that nothing
136
+ * moved.
137
+ */
138
+ readonly moved?: ObservationRecord['moved'];
139
+ /** Which images this build kept. Absent means the run did not save one. */
140
+ readonly has: {
141
+ readonly before: boolean;
142
+ readonly after: boolean;
143
+ readonly diff: boolean;
144
+ };
145
+ /**
146
+ * The candidate's dimensions, when it kept one.
147
+ *
148
+ * Carried so a viewer can place region rectangles over the image without
149
+ * measuring it in the browser first. Region coordinates are in the raster's own
150
+ * pixel space; a viewer that scaled them by a measured `naturalWidth` would draw
151
+ * the boxes in the right place only after the image had loaded, and in the wrong
152
+ * place for one frame before that.
153
+ */
154
+ readonly size?: {
155
+ readonly width: number;
156
+ readonly height: number;
157
+ };
158
+ /**
159
+ * The baseline's own dimensions, when the run measured them.
160
+ *
161
+ * Separate from {@link SubjectView.size} rather than folded into it, because
162
+ * the two being different is a finding. A viewer that had only one pair would
163
+ * have to draw both layers to it, and a capture that grew by 40 pixels of
164
+ * width would read as identical everywhere except a hairline at the edge.
165
+ */
166
+ readonly baseline?: {
167
+ readonly width: number;
168
+ readonly height: number;
169
+ };
170
+ /** `true` when the candidate carries the sidecar an approval would promote. */
171
+ readonly approvable: boolean;
172
+ readonly decision: DecisionRecord | null;
173
+ }
174
+ /**
175
+ * A build, and the docket a reviewer reads.
176
+ *
177
+ * `causes` is the ordering the incumbent comparison gets backwards. Ranked by
178
+ * area, a component that only *reflowed* outranks the component that was edited —
179
+ * measured at 6× on one edit — so the docket is grouped by the components the
180
+ * semantic tier named as causes, and collateral is counted rather than listed.
181
+ * One token change across 300 subjects is one review item with a count, never 300
182
+ * lines.
183
+ */
184
+ export interface BuildDetail extends BuildSummary {
185
+ readonly subjects: readonly SubjectView[];
186
+ readonly notObserved: readonly NotObserved[];
187
+ readonly causes: readonly Cause[];
188
+ /**
189
+ * What the run read about its own subjects — the section with no baseline in it.
190
+ *
191
+ * Beside `causes` and not among them, because these are not changes and must
192
+ * never be counted as any: a variation is a difference somebody meant. It is
193
+ * here because it answers the question a docket cannot. A subject added behind
194
+ * a flag is `new`, its diff is empty, and what the flag *does* is visible only
195
+ * by opening two pictures side by side — which is the review nobody performs.
196
+ * The run already compared the pair, so the answer is carried rather than
197
+ * looked for.
198
+ *
199
+ * Empty when the report declared none. `VariationRecord` unchanged from the
200
+ * report's, because a store that reshaped it would be a second vocabulary for
201
+ * one fact.
202
+ */
203
+ readonly variations: readonly VariationRecord[];
204
+ /**
205
+ * What the commit reaches, and per subject whether it reaches that one.
206
+ *
207
+ * `null` when the run carried no diff — no ref to read against, or no file
208
+ * graph to walk — which is different from a diff that reached nothing and is
209
+ * shown as different.
210
+ *
211
+ * The four states a reviewer actually reads are not stored anywhere. They are
212
+ * this crossed against the verdicts already in `subjects`, and two of them are
213
+ * questions no comparison can pose on its own: a subject the commit reaches
214
+ * that did not move, and a subject that moved with nothing in the commit
215
+ * reaching it.
216
+ */
217
+ readonly reach: ReachView | null;
218
+ /**
219
+ * What the config declared, and what each declaration did in this run.
220
+ *
221
+ * The audit surface, carried across the boundary rather than left in a CI log.
222
+ * A mask that outlived its cause is only ever found by comparing runs — *this
223
+ * rule absorbed nothing again* — and a review service that dropped the ledger
224
+ * could show the question but never the answer.
225
+ */
226
+ readonly declarations: Declarations;
227
+ /**
228
+ * Who draws what, over the whole suite.
229
+ *
230
+ * `null` when the report carried no composition — a raster-only run has no
231
+ * boundaries to join. Empty is never written and would say something else
232
+ * entirely: that the run read its subjects and found no component in them.
233
+ */
234
+ readonly composition: readonly Placement[] | null;
235
+ /**
236
+ * What the run concluded about each thing that moved, in component order.
237
+ *
238
+ * The census above is the graph; this is the reading of it. A review page can
239
+ * derive neither: the run had the diff, the source index that maps a component
240
+ * to the file declaring it, the props digest each rendering was grouped under,
241
+ * and the subjects where the same component with the same props held still.
242
+ * None of those cross the wire, and a page that tried to re-derive the answer
243
+ * from `composition` and `reach` alone would be guessing at the one question a
244
+ * reviewer opens a change to ask.
245
+ *
246
+ * Empty when the store holds none, which includes a build ingested before this
247
+ * was carried. So it is read as *no attribution is on record*, and nothing
248
+ * downstream may turn it into *the run examined this and found no cause*.
249
+ */
250
+ readonly movements: readonly MovementView[];
251
+ }
252
+ /**
253
+ * Why one component moved in one subject, as the run decided it.
254
+ *
255
+ * `MovementRecord` from the report, minus `alsoIn` — which is the subjects of
256
+ * every sibling row for the same component, and a stored copy of it is a second
257
+ * answer able to disagree with the first.
258
+ *
259
+ * The pair is the key. A component moves for its own reason in each subject it
260
+ * moved in: the same `Button` is `edited` on the page whose file the diff names
261
+ * and `upstream` on the one where a changed parent hands it different props.
262
+ */
263
+ export interface MovementView {
264
+ readonly subject: string;
265
+ readonly component: string;
266
+ readonly cause: 'edited' | 'token' | 'upstream' | 'contradicted' | 'unexplained';
267
+ /** One sentence, naming the evidence rather than the category. */
268
+ readonly because: string;
269
+ /** Empty means *not known* — a name-only comparison — never *no band*. */
270
+ readonly bands: readonly string[];
271
+ /** Subjects where the same component, with the same props, held still. */
272
+ readonly held: readonly string[];
273
+ /**
274
+ * Renders of this component with the same props, other than this one, that the
275
+ * run read — the pool `held` was drawn from.
276
+ *
277
+ * Absent is *the run never said*, from a build ingested before this was
278
+ * carried, and it is the reason `held: []` cannot be drawn as a finding on its
279
+ * own: without the denominator, *nothing to compare against* and *compared in
280
+ * four renders and moved in all four* are the same empty list.
281
+ */
282
+ readonly compared?: number;
283
+ /** The file the diff named, on the `edited` rung. */
284
+ readonly file?: string;
285
+ /** The custom properties that took a new value, on the `token` rung. */
286
+ readonly tokens?: readonly string[];
287
+ /** The edited component that reaches this one, on the `upstream` rung. */
288
+ readonly upstream?: string;
289
+ /** Components between `upstream` and this one, outermost first. */
290
+ readonly through?: readonly string[];
291
+ /** On an unexplained movement: whether the subject was already proven unstable. */
292
+ readonly standing?: 'flake' | 'suspect';
293
+ }
294
+ /**
295
+ * The two ledgers as this store holds them.
296
+ *
297
+ * `null` is *the report carried none*, and it is deliberately one state rather
298
+ * than two. On this format a config with no ignores and a writer that never kept
299
+ * a ledger produce the same absence, so a store that offered two answers would
300
+ * be inventing the difference. What it must not do is answer with an empty
301
+ * ledger, which would report an unaudited build as one that was audited and
302
+ * found clean.
303
+ */
304
+ export interface Declarations {
305
+ readonly ignores: IgnoreLedger | null;
306
+ readonly sensitivities: SensitivityLedger | null;
307
+ }
308
+ /**
309
+ * Where one component sits in the suite, as the run's census recorded it.
310
+ *
311
+ * The other direction from {@link ReachView}, and the page needs both. Reach
312
+ * climbs: from a file the diff named, through its importers, to the components
313
+ * an edit could have arrived at. It can therefore never name anything a changed
314
+ * file *draws* — `ProductCard` renders `Card`, `Card` renders `CardFooter`, and
315
+ * an upward walk arrives at none of them. That is why a build page could count
316
+ * the components no rung held and could not say why any of them moved.
317
+ *
318
+ * Nothing here is measured for the tribunal. It is `composition.components` from
319
+ * the report, narrowed to the three edges a sentence needs.
320
+ */
321
+ export interface Placement {
322
+ readonly component: string;
323
+ /** Subjects holding at least one boundary of it, in plan order. */
324
+ readonly subjects: readonly string[];
325
+ /** Components that enclose it somewhere in the suite, sorted. */
326
+ readonly within: readonly string[];
327
+ /**
328
+ * Components that mounted it somewhere, sorted.
329
+ *
330
+ * Empty on a production build, where React keeps no `_debugOwner` — and empty
331
+ * is **not** *nothing mounted it*. The report's own field carries the same
332
+ * ambiguity and the store does not resolve it, because resolving it here would
333
+ * be a claim about a build this service never saw.
334
+ */
335
+ readonly createdBy: readonly string[];
336
+ /** Components it encloses somewhere, sorted. */
337
+ readonly renders: readonly string[];
338
+ }
339
+ /**
340
+ * The reach section as the page reads it.
341
+ *
342
+ * `subjects` absent is the run saying it had a diff and could not attribute it,
343
+ * with `whole` carrying the reason. Empty would say the commit was understood and
344
+ * reaches none of them, and a page that drew the second when it held the first
345
+ * would let somebody merge on a refusal.
346
+ */
347
+ export interface ReachView {
348
+ readonly against: string;
349
+ readonly changed: readonly string[];
350
+ readonly components: readonly ReachedComponent[];
351
+ readonly subjects?: Readonly<Record<string, SubjectReach>>;
352
+ readonly whole?: string;
353
+ readonly unscanned?: readonly string[];
354
+ readonly opaque?: readonly ReachHole[];
355
+ }
356
+ export interface Cause {
357
+ readonly component: string;
358
+ readonly file?: string;
359
+ readonly subjects: readonly string[];
360
+ readonly pixels: number;
361
+ /** Regions in the same builds that no component claimed as a cause. */
362
+ readonly collateralPixels: number;
363
+ }
364
+ export interface SweepReport {
365
+ /** Builds removed. */
366
+ readonly builds: number;
367
+ /** Subject rows removed with them. */
368
+ readonly subjects: number;
369
+ /** Stored images removed with them. */
370
+ readonly objects: number;
371
+ /**
372
+ * Decisions those builds carried, which are **kept**, not removed.
373
+ *
374
+ * Named for what happened to them, because every other number here is a
375
+ * removal and an operator reading `decisions: 4` beside them concludes four
376
+ * approvals were deleted — the one thing this store promises never happens
377
+ * ([ADR-0021](../../../docs/context/adr/0021-approval-promotes-an-image-that-already-exists.md)).
378
+ * The count is worth reporting: it is how much attribution outlived the build
379
+ * that proposed it, and a sweep that returned zero here forever would be a
380
+ * retention policy quietly disagreeing with the trigger on the table.
381
+ */
382
+ readonly decisionsKept: number;
383
+ }
384
+ export interface ReviewStore {
385
+ ingest(build: BuildIngest): Promise<void>;
386
+ builds(limit?: number): Promise<readonly BuildSummary[]>;
387
+ build(id: string): Promise<BuildDetail | null>;
388
+ image(build: string, subject: string, kind: 'before' | 'after' | 'diff'): Promise<ArrayBuffer | null>;
389
+ decide(input: {
390
+ readonly build: string;
391
+ readonly subject: string;
392
+ readonly decision: Decision;
393
+ readonly by: string;
394
+ readonly note?: string;
395
+ }): Promise<DecisionRecord>;
396
+ /**
397
+ * Why the baselines are what they are — every approval, grouped by what changed.
398
+ *
399
+ * Deliberately not derived from `builds`: those expire, and a baseline's
400
+ * explanation has to outlive the build that proposed it by as long as the
401
+ * baseline lasts. See [`changelog.ts`](./changelog.ts).
402
+ */
403
+ changelog(query?: TribunalChangelogQuery): Promise<TribunalChangelog>;
404
+ /**
405
+ * Remove builds older than `keepDays`, and everything that hangs off them.
406
+ *
407
+ * `decisions` and `changelog` are not among them, and both carry a permanence
408
+ * trigger saying so: a promoted baseline whose approval was swept is a change
409
+ * nobody can attribute to anyone, and one whose changelog entry was swept is a
410
+ * baseline nobody can account for.
411
+ */
412
+ sweep(keepDays: number): Promise<SweepReport>;
413
+ }
414
+ //# sourceMappingURL=review-types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=review-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-types.js","sourceRoot":"","sources":["../src/review-types.ts"],"names":[],"mappings":"","sourcesContent":["import type { AccessibilitySnapshot, RenderIdentity } from '@variance-authority/core';\nimport type {\n FindingRecord,\n IgnoreLedger,\n NotObserved,\n ObservationRecord,\n ReachHole,\n ReachedComponent,\n RegionRecord,\n RunReport,\n SensitivityLedger,\n SubjectReach,\n VariationRecord,\n} from '@variance-authority/report';\nimport type { TribunalBindings } from './bindings.js';\n\n/**\n * What a review store takes and what it answers with — and nothing that runs.\n *\n * The vocabulary rather than the implementation, apart from it because three\n * other module graphs need the words without the machinery: `worker-input.ts`\n * types the request bodies it validates against these, `ui/client.ts` types its\n * responses against them, and the React surface renders them. None of those\n * should have to name a D1 binding to say what a `BuildSummary` is.\n *\n * The arguments that produced these shapes are on the shapes, because that is\n * where somebody about to change one will look. Why the store *behaves* as it\n * does is in [`review.ts`](./review.ts).\n */\n\nexport interface ReviewOptions extends TribunalBindings {\n readonly project: string;\n /** Injected so tests can pin every `at`. Defaults to the wall clock. */\n readonly now?: () => Date;\n}\n\n/** One image the run kept, as it arrives. */\nexport interface CandidateImage {\n /** Base64 PNG — the same encoding a `Raster` carries, for the same reason. */\n readonly bytes: string;\n}\n\n/**\n * What a run produced for one subject, over and above the report's record of it.\n *\n * `after` is the candidate and is the only one that can be promoted, which is why\n * it alone carries the sidecar fields. `before` and `diff` exist to be looked at.\n */\nexport interface SubjectImages {\n readonly after?: CandidateImage & {\n readonly documentDigest: string;\n readonly width: number;\n readonly height: number;\n readonly missingFonts: readonly string[];\n readonly accessibility?: AccessibilitySnapshot;\n };\n /**\n * The baseline this run compared against, with its own dimensions when they\n * could be read.\n *\n * Optional, and absent means *not measured* — never *the same size as the\n * candidate*. That assumption is the thing this field exists to stop: a\n * baseline drawn to the candidate's box is a width change resampled out of\n * existence, on the one screen where somebody decides whether it is allowed.\n */\n readonly before?: CandidateImage & {\n readonly width?: number;\n readonly height?: number;\n };\n readonly diff?: CandidateImage;\n}\n\nexport interface BuildIngest {\n /** The operator's own id for the run — a CI job number, a workflow run id. */\n readonly build: string;\n readonly commit: string;\n readonly branch?: string;\n readonly report: RunReport;\n /** Keyed by subject. A subject with no entry is recorded with no images. */\n readonly images?: Readonly<Record<string, SubjectImages>>;\n}\n\nimport type { TribunalChangelog, TribunalChangelogQuery } from './changelog.js';\n\nexport type Decision = 'approved' | 'rejected';\n\nexport interface DecisionRecord {\n readonly decision: Decision;\n readonly by: string;\n readonly note?: string;\n readonly at: string;\n}\n\nexport interface BuildSummary {\n readonly project: string;\n readonly build: string;\n readonly commit: string;\n readonly branch?: string;\n readonly intent?: string;\n readonly at: string;\n readonly identity: RenderIdentity;\n readonly retention: 'durable' | 'ephemeral';\n /** One entry per verdict the report used, including the ones with no subjects. */\n readonly verdicts: Readonly<Record<ObservationRecord['verdict'], number>>;\n readonly decided: number;\n /** Subjects whose verdict needs review and that nobody has decided yet. */\n readonly pending: number;\n readonly coverage: Coverage;\n}\n\n/**\n * What the run said about the subjects it did not observe.\n *\n * `stated: false` is the whole reason this is a shape rather than two numbers. A\n * summary that showed `failed: 0` for a report that never carried the list would\n * be asserting coverage on the authority of a writer that declined to claim any.\n */\nexport interface Coverage {\n readonly stated: boolean;\n readonly failed: number;\n readonly excluded: number;\n /** Subjects the run proved this change cannot reach, so never rendered. */\n readonly unreached: number;\n}\n\nexport interface SubjectView {\n readonly subject: string;\n readonly verdict: ObservationRecord['verdict'];\n readonly because: string;\n readonly changedPixels: number;\n readonly regions: readonly RegionRecord[];\n readonly truncated?: { readonly regions: number; readonly pixels: number };\n readonly missingFonts?: readonly string[];\n readonly findings?: readonly FindingRecord[];\n readonly signals?: ObservationRecord['signals'];\n /**\n * What the operator's ignores took out of this comparison, as the run wrote it.\n *\n * Carried per subject even though the build also carries a ledger, because the\n * two answer different questions. The ledger says a rule absorbed 647 pixels\n * somewhere; this says it absorbed them *here*. A settled list built from the\n * ledger alone can only say a declaration decided this subject, which is the\n * sentence a reviewer already read in the verdict.\n */\n readonly ignored?: ObservationRecord['ignored'];\n /** The sensitivity that decided this subject, when one did. */\n readonly relaxed?: ObservationRecord['relaxed'];\n /**\n * Which components moved here per their hashes, and in which bands.\n *\n * The half of the record {@link SubjectView.regions} cannot carry. A region is\n * named from where its box landed, so an edit that reflowed its neighbours\n * arrives as one blob attributed to the document root; these entries compared\n * digests and never saw a pixel, so they still hold the component and the\n * sense. Absent means the baseline carried no hashes — never that nothing\n * moved.\n */\n readonly moved?: ObservationRecord['moved'];\n /** Which images this build kept. Absent means the run did not save one. */\n readonly has: { readonly before: boolean; readonly after: boolean; readonly diff: boolean };\n /**\n * The candidate's dimensions, when it kept one.\n *\n * Carried so a viewer can place region rectangles over the image without\n * measuring it in the browser first. Region coordinates are in the raster's own\n * pixel space; a viewer that scaled them by a measured `naturalWidth` would draw\n * the boxes in the right place only after the image had loaded, and in the wrong\n * place for one frame before that.\n */\n readonly size?: { readonly width: number; readonly height: number };\n /**\n * The baseline's own dimensions, when the run measured them.\n *\n * Separate from {@link SubjectView.size} rather than folded into it, because\n * the two being different is a finding. A viewer that had only one pair would\n * have to draw both layers to it, and a capture that grew by 40 pixels of\n * width would read as identical everywhere except a hairline at the edge.\n */\n readonly baseline?: { readonly width: number; readonly height: number };\n /** `true` when the candidate carries the sidecar an approval would promote. */\n readonly approvable: boolean;\n readonly decision: DecisionRecord | null;\n}\n\n/**\n * A build, and the docket a reviewer reads.\n *\n * `causes` is the ordering the incumbent comparison gets backwards. Ranked by\n * area, a component that only *reflowed* outranks the component that was edited —\n * measured at 6× on one edit — so the docket is grouped by the components the\n * semantic tier named as causes, and collateral is counted rather than listed.\n * One token change across 300 subjects is one review item with a count, never 300\n * lines.\n */\nexport interface BuildDetail extends BuildSummary {\n readonly subjects: readonly SubjectView[];\n readonly notObserved: readonly NotObserved[];\n readonly causes: readonly Cause[];\n /**\n * What the run read about its own subjects — the section with no baseline in it.\n *\n * Beside `causes` and not among them, because these are not changes and must\n * never be counted as any: a variation is a difference somebody meant. It is\n * here because it answers the question a docket cannot. A subject added behind\n * a flag is `new`, its diff is empty, and what the flag *does* is visible only\n * by opening two pictures side by side — which is the review nobody performs.\n * The run already compared the pair, so the answer is carried rather than\n * looked for.\n *\n * Empty when the report declared none. `VariationRecord` unchanged from the\n * report's, because a store that reshaped it would be a second vocabulary for\n * one fact.\n */\n readonly variations: readonly VariationRecord[];\n\n /**\n * What the commit reaches, and per subject whether it reaches that one.\n *\n * `null` when the run carried no diff — no ref to read against, or no file\n * graph to walk — which is different from a diff that reached nothing and is\n * shown as different.\n *\n * The four states a reviewer actually reads are not stored anywhere. They are\n * this crossed against the verdicts already in `subjects`, and two of them are\n * questions no comparison can pose on its own: a subject the commit reaches\n * that did not move, and a subject that moved with nothing in the commit\n * reaching it.\n */\n readonly reach: ReachView | null;\n\n /**\n * What the config declared, and what each declaration did in this run.\n *\n * The audit surface, carried across the boundary rather than left in a CI log.\n * A mask that outlived its cause is only ever found by comparing runs — *this\n * rule absorbed nothing again* — and a review service that dropped the ledger\n * could show the question but never the answer.\n */\n readonly declarations: Declarations;\n\n /**\n * Who draws what, over the whole suite.\n *\n * `null` when the report carried no composition — a raster-only run has no\n * boundaries to join. Empty is never written and would say something else\n * entirely: that the run read its subjects and found no component in them.\n */\n readonly composition: readonly Placement[] | null;\n\n /**\n * What the run concluded about each thing that moved, in component order.\n *\n * The census above is the graph; this is the reading of it. A review page can\n * derive neither: the run had the diff, the source index that maps a component\n * to the file declaring it, the props digest each rendering was grouped under,\n * and the subjects where the same component with the same props held still.\n * None of those cross the wire, and a page that tried to re-derive the answer\n * from `composition` and `reach` alone would be guessing at the one question a\n * reviewer opens a change to ask.\n *\n * Empty when the store holds none, which includes a build ingested before this\n * was carried. So it is read as *no attribution is on record*, and nothing\n * downstream may turn it into *the run examined this and found no cause*.\n */\n readonly movements: readonly MovementView[];\n}\n\n/**\n * Why one component moved in one subject, as the run decided it.\n *\n * `MovementRecord` from the report, minus `alsoIn` — which is the subjects of\n * every sibling row for the same component, and a stored copy of it is a second\n * answer able to disagree with the first.\n *\n * The pair is the key. A component moves for its own reason in each subject it\n * moved in: the same `Button` is `edited` on the page whose file the diff names\n * and `upstream` on the one where a changed parent hands it different props.\n */\nexport interface MovementView {\n readonly subject: string;\n readonly component: string;\n readonly cause: 'edited' | 'token' | 'upstream' | 'contradicted' | 'unexplained';\n /** One sentence, naming the evidence rather than the category. */\n readonly because: string;\n /** Empty means *not known* — a name-only comparison — never *no band*. */\n readonly bands: readonly string[];\n /** Subjects where the same component, with the same props, held still. */\n readonly held: readonly string[];\n\n /**\n * Renders of this component with the same props, other than this one, that the\n * run read — the pool `held` was drawn from.\n *\n * Absent is *the run never said*, from a build ingested before this was\n * carried, and it is the reason `held: []` cannot be drawn as a finding on its\n * own: without the denominator, *nothing to compare against* and *compared in\n * four renders and moved in all four* are the same empty list.\n */\n readonly compared?: number;\n\n /** The file the diff named, on the `edited` rung. */\n readonly file?: string;\n /** The custom properties that took a new value, on the `token` rung. */\n readonly tokens?: readonly string[];\n /** The edited component that reaches this one, on the `upstream` rung. */\n readonly upstream?: string;\n /** Components between `upstream` and this one, outermost first. */\n readonly through?: readonly string[];\n /** On an unexplained movement: whether the subject was already proven unstable. */\n readonly standing?: 'flake' | 'suspect';\n}\n\n/**\n * The two ledgers as this store holds them.\n *\n * `null` is *the report carried none*, and it is deliberately one state rather\n * than two. On this format a config with no ignores and a writer that never kept\n * a ledger produce the same absence, so a store that offered two answers would\n * be inventing the difference. What it must not do is answer with an empty\n * ledger, which would report an unaudited build as one that was audited and\n * found clean.\n */\nexport interface Declarations {\n readonly ignores: IgnoreLedger | null;\n readonly sensitivities: SensitivityLedger | null;\n}\n\n/**\n * Where one component sits in the suite, as the run's census recorded it.\n *\n * The other direction from {@link ReachView}, and the page needs both. Reach\n * climbs: from a file the diff named, through its importers, to the components\n * an edit could have arrived at. It can therefore never name anything a changed\n * file *draws* — `ProductCard` renders `Card`, `Card` renders `CardFooter`, and\n * an upward walk arrives at none of them. That is why a build page could count\n * the components no rung held and could not say why any of them moved.\n *\n * Nothing here is measured for the tribunal. It is `composition.components` from\n * the report, narrowed to the three edges a sentence needs.\n */\nexport interface Placement {\n readonly component: string;\n /** Subjects holding at least one boundary of it, in plan order. */\n readonly subjects: readonly string[];\n /** Components that enclose it somewhere in the suite, sorted. */\n readonly within: readonly string[];\n /**\n * Components that mounted it somewhere, sorted.\n *\n * Empty on a production build, where React keeps no `_debugOwner` — and empty\n * is **not** *nothing mounted it*. The report's own field carries the same\n * ambiguity and the store does not resolve it, because resolving it here would\n * be a claim about a build this service never saw.\n */\n readonly createdBy: readonly string[];\n /** Components it encloses somewhere, sorted. */\n readonly renders: readonly string[];\n}\n\n/**\n * The reach section as the page reads it.\n *\n * `subjects` absent is the run saying it had a diff and could not attribute it,\n * with `whole` carrying the reason. Empty would say the commit was understood and\n * reaches none of them, and a page that drew the second when it held the first\n * would let somebody merge on a refusal.\n */\nexport interface ReachView {\n readonly against: string;\n readonly changed: readonly string[];\n readonly components: readonly ReachedComponent[];\n readonly subjects?: Readonly<Record<string, SubjectReach>>;\n readonly whole?: string;\n readonly unscanned?: readonly string[];\n readonly opaque?: readonly ReachHole[];\n}\n\nexport interface Cause {\n readonly component: string;\n readonly file?: string;\n readonly subjects: readonly string[];\n readonly pixels: number;\n /** Regions in the same builds that no component claimed as a cause. */\n readonly collateralPixels: number;\n}\n\nexport interface SweepReport {\n /** Builds removed. */\n readonly builds: number;\n /** Subject rows removed with them. */\n readonly subjects: number;\n /** Stored images removed with them. */\n readonly objects: number;\n /**\n * Decisions those builds carried, which are **kept**, not removed.\n *\n * Named for what happened to them, because every other number here is a\n * removal and an operator reading `decisions: 4` beside them concludes four\n * approvals were deleted — the one thing this store promises never happens\n * ([ADR-0021](../../../docs/context/adr/0021-approval-promotes-an-image-that-already-exists.md)).\n * The count is worth reporting: it is how much attribution outlived the build\n * that proposed it, and a sweep that returned zero here forever would be a\n * retention policy quietly disagreeing with the trigger on the table.\n */\n readonly decisionsKept: number;\n}\n\nexport interface ReviewStore {\n ingest(build: BuildIngest): Promise<void>;\n builds(limit?: number): Promise<readonly BuildSummary[]>;\n build(id: string): Promise<BuildDetail | null>;\n image(build: string, subject: string, kind: 'before' | 'after' | 'diff'): Promise<ArrayBuffer | null>;\n decide(input: {\n readonly build: string;\n readonly subject: string;\n readonly decision: Decision;\n readonly by: string;\n readonly note?: string;\n }): Promise<DecisionRecord>;\n /**\n * Why the baselines are what they are — every approval, grouped by what changed.\n *\n * Deliberately not derived from `builds`: those expire, and a baseline's\n * explanation has to outlive the build that proposed it by as long as the\n * baseline lasts. See [`changelog.ts`](./changelog.ts).\n */\n changelog(query?: TribunalChangelogQuery): Promise<TribunalChangelog>;\n /**\n * Remove builds older than `keepDays`, and everything that hangs off them.\n *\n * `decisions` and `changelog` are not among them, and both carry a permanence\n * trigger saying so: a promoted baseline whose approval was swept is a change\n * nobody can attribute to anyone, and one whose changelog entry was swept is a\n * baseline nobody can account for.\n */\n sweep(keepDays: number): Promise<SweepReport>;\n}\n"]}
@@ -0,0 +1,35 @@
1
+ import { type RasterStore } from '@variance-authority/raster';
2
+ import { type D1Like, type R2Like } from './bindings.js';
3
+ import { type Row } from './review-rows.js';
4
+ import type { SubjectImages } from './review-types.js';
5
+ /**
6
+ * The two writes that reach the bucket: keeping what a run uploaded, and
7
+ * promoting one of those objects to a baseline.
8
+ *
9
+ * Apart from [`review.ts`](./review.ts) because these are the operations with a
10
+ * consequence outside this package. `store` decides what a build keeps;
11
+ * `promote` is what `variance accept` means when it is a click, and it is the
12
+ * reason approving cannot be a flag on a row.
13
+ *
14
+ * The *ordering* around them is argued at the call sites rather than here —
15
+ * objects before rows on the way in, promotion before the decision on the way out
16
+ * — because it is the caller that can get it wrong.
17
+ */
18
+ export interface StoredKeys {
19
+ before?: string;
20
+ after?: string;
21
+ diff?: string;
22
+ }
23
+ export declare function store(bucket: R2Like, project: string, build: string, subject: string, images: SubjectImages | undefined): Promise<StoredKeys>;
24
+ /**
25
+ * Approving, as the promotion of an image that already exists.
26
+ *
27
+ * Every field of the baseline comes from what the run uploaded: the bytes from
28
+ * the bucket, the digest and dimensions from the build row, the identity from the
29
+ * build. Nothing is rendered, nothing is measured, and nothing is defaulted — a
30
+ * missing candidate is refused, because the only way to fill the gap would be to
31
+ * paint one, and an approval whose image nobody reviewed is worse than no
32
+ * approval at all.
33
+ */
34
+ export declare function promote(bucket: R2Like, baselines: RasterStore, db: D1Like, project: string, build: string, subject: string, row: Row): Promise<void>;
35
+ //# sourceMappingURL=review-write.d.ts.map
@@ -0,0 +1,86 @@
1
+ import { RasterStoreError, identityFrom, sidecarFrom, } from '@variance-authority/raster';
2
+ import { base64Of, bytesOf } from './bindings.js';
3
+ import { ReviewError, number, optionalText, text } from './review-rows.js';
4
+ export async function store(bucket, project, build, subject, images) {
5
+ if (images === undefined)
6
+ return {};
7
+ const keys = {};
8
+ const prefix = `${project}/builds/${encodeURIComponent(build)}/${encodeURIComponent(subject)}`;
9
+ for (const kind of ['before', 'after', 'diff']) {
10
+ const image = images[kind];
11
+ if (image === undefined)
12
+ continue;
13
+ const key = `${prefix}.${kind}.png`;
14
+ await bucket.put(key, bytesOf(image.bytes));
15
+ keys[kind] = key;
16
+ }
17
+ return keys;
18
+ }
19
+ /**
20
+ * Approving, as the promotion of an image that already exists.
21
+ *
22
+ * Every field of the baseline comes from what the run uploaded: the bytes from
23
+ * the bucket, the digest and dimensions from the build row, the identity from the
24
+ * build. Nothing is rendered, nothing is measured, and nothing is defaulted — a
25
+ * missing candidate is refused, because the only way to fill the gap would be to
26
+ * paint one, and an approval whose image nobody reviewed is worse than no
27
+ * approval at all.
28
+ */
29
+ export async function promote(bucket, baselines, db, project, build, subject, row) {
30
+ const key = optionalText(row, 'after_key', 'a build subject');
31
+ const digest = optionalText(row, 'candidate_document_digest', 'a build subject');
32
+ if (key === undefined || digest === undefined) {
33
+ throw new ReviewError(`subject "${subject}" of build "${build}" cannot be approved: the run did not upload a ` +
34
+ 'candidate for it, and a baseline is an image plus the document it was painted from. ' +
35
+ 'Approving would mean rendering one now, which is recording rather than promoting');
36
+ }
37
+ const buildRow = await db
38
+ .prepare('SELECT identity FROM builds WHERE project = ? AND build = ?')
39
+ .bind(project, build)
40
+ .first();
41
+ const identity = identityFrom(JSON.parse(text(buildRow ?? {}, 'identity', 'a build')));
42
+ if (identity === null) {
43
+ throw new ReviewError(`build "${build}" does not carry a renderer identity that can be read back, so there is no ` +
44
+ 'machine to file the promoted baseline under. A baseline with no identity is one every ' +
45
+ 'other machine would compare against and none of them should');
46
+ }
47
+ const object = await bucket.get(key);
48
+ if (object === null) {
49
+ throw new RasterStoreError(`the candidate image for ${subject} of build "${build}" is recorded at \`${key}\` and the ` +
50
+ 'bucket has no such object. The approval is refused rather than promoting a baseline ' +
51
+ 'this deployment cannot produce the bytes for');
52
+ }
53
+ const raster = {
54
+ documentDigest: digest,
55
+ identity,
56
+ width: number(row, 'candidate_width', 'a build subject'),
57
+ height: number(row, 'candidate_height', 'a build subject'),
58
+ bytes: base64Of(await object.arrayBuffer()),
59
+ missingFonts: strings(optionalText(row, 'candidate_missing_fonts', 'a build subject')),
60
+ ...accessibilityField(optionalText(row, 'candidate_accessibility', 'a build subject'), identity),
61
+ };
62
+ await baselines.put({ subject }, raster);
63
+ }
64
+ function accessibilityField(json, identity) {
65
+ if (json === undefined)
66
+ return {};
67
+ const parsed = sidecarFrom({
68
+ documentDigest: 'candidate',
69
+ identity,
70
+ width: 0,
71
+ height: 0,
72
+ missingFonts: [],
73
+ accessibility: JSON.parse(json),
74
+ });
75
+ if (parsed?.accessibility === undefined) {
76
+ throw new ReviewError('the candidate browser accessibility snapshot is malformed');
77
+ }
78
+ return { accessibility: parsed.accessibility };
79
+ }
80
+ function strings(json) {
81
+ if (json === undefined)
82
+ return [];
83
+ const parsed = JSON.parse(json);
84
+ return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === 'string') : [];
85
+ }
86
+ //# sourceMappingURL=review-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-write.js","sourceRoot":"","sources":["../src/review-write.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Raster } from '@variance-authority/core';\nimport {\n RasterStoreError,\n identityFrom,\n sidecarFrom,\n type RasterStore,\n} from '@variance-authority/raster';\nimport { base64Of, bytesOf, type D1Like, type R2Like } from './bindings.js';\nimport { ReviewError, number, optionalText, text, type Row } from './review-rows.js';\nimport type { SubjectImages } from './review-types.js';\n\n/**\n * The two writes that reach the bucket: keeping what a run uploaded, and\n * promoting one of those objects to a baseline.\n *\n * Apart from [`review.ts`](./review.ts) because these are the operations with a\n * consequence outside this package. `store` decides what a build keeps;\n * `promote` is what `variance accept` means when it is a click, and it is the\n * reason approving cannot be a flag on a row.\n *\n * The *ordering* around them is argued at the call sites rather than here —\n * objects before rows on the way in, promotion before the decision on the way out\n * — because it is the caller that can get it wrong.\n */\n\nexport interface StoredKeys {\n before?: string;\n after?: string;\n diff?: string;\n}\n\nexport async function store(\n bucket: R2Like,\n project: string,\n build: string,\n subject: string,\n images: SubjectImages | undefined,\n): Promise<StoredKeys> {\n if (images === undefined) return {};\n\n const keys: StoredKeys = {};\n const prefix = `${project}/builds/${encodeURIComponent(build)}/${encodeURIComponent(subject)}`;\n\n for (const kind of ['before', 'after', 'diff'] as const) {\n const image = images[kind];\n if (image === undefined) continue;\n const key = `${prefix}.${kind}.png`;\n await bucket.put(key, bytesOf(image.bytes));\n keys[kind] = key;\n }\n return keys;\n}\n\n/**\n * Approving, as the promotion of an image that already exists.\n *\n * Every field of the baseline comes from what the run uploaded: the bytes from\n * the bucket, the digest and dimensions from the build row, the identity from the\n * build. Nothing is rendered, nothing is measured, and nothing is defaulted — a\n * missing candidate is refused, because the only way to fill the gap would be to\n * paint one, and an approval whose image nobody reviewed is worse than no\n * approval at all.\n */\nexport async function promote(\n bucket: R2Like,\n baselines: RasterStore,\n db: D1Like,\n project: string,\n build: string,\n subject: string,\n row: Row,\n): Promise<void> {\n const key = optionalText(row, 'after_key', 'a build subject');\n const digest = optionalText(row, 'candidate_document_digest', 'a build subject');\n\n if (key === undefined || digest === undefined) {\n throw new ReviewError(\n `subject \"${subject}\" of build \"${build}\" cannot be approved: the run did not upload a ` +\n 'candidate for it, and a baseline is an image plus the document it was painted from. ' +\n 'Approving would mean rendering one now, which is recording rather than promoting',\n );\n }\n\n const buildRow = await db\n .prepare('SELECT identity FROM builds WHERE project = ? AND build = ?')\n .bind(project, build)\n .first<Row>();\n const identity = identityFrom(JSON.parse(text(buildRow ?? {}, 'identity', 'a build')) as unknown);\n\n if (identity === null) {\n throw new ReviewError(\n `build \"${build}\" does not carry a renderer identity that can be read back, so there is no ` +\n 'machine to file the promoted baseline under. A baseline with no identity is one every ' +\n 'other machine would compare against and none of them should',\n );\n }\n\n const object = await bucket.get(key);\n if (object === null) {\n throw new RasterStoreError(\n `the candidate image for ${subject} of build \"${build}\" is recorded at \\`${key}\\` and the ` +\n 'bucket has no such object. The approval is refused rather than promoting a baseline ' +\n 'this deployment cannot produce the bytes for',\n );\n }\n\n const raster: Raster = {\n documentDigest: digest as Raster['documentDigest'],\n identity,\n width: number(row, 'candidate_width', 'a build subject'),\n height: number(row, 'candidate_height', 'a build subject'),\n bytes: base64Of(await object.arrayBuffer()),\n missingFonts: strings(optionalText(row, 'candidate_missing_fonts', 'a build subject')),\n ...accessibilityField(optionalText(row, 'candidate_accessibility', 'a build subject'), identity),\n };\n\n await baselines.put({ subject }, raster);\n}\n\nfunction accessibilityField(\n json: string | undefined,\n identity: Raster['identity'],\n): Pick<Raster, 'accessibility'> {\n if (json === undefined) return {};\n const parsed = sidecarFrom({\n documentDigest: 'candidate',\n identity,\n width: 0,\n height: 0,\n missingFonts: [],\n accessibility: JSON.parse(json) as unknown,\n });\n if (parsed?.accessibility === undefined) {\n throw new ReviewError('the candidate browser accessibility snapshot is malformed');\n }\n return { accessibility: parsed.accessibility };\n}\n\nfunction strings(json: string | undefined): readonly string[] {\n if (json === undefined) return [];\n const parsed: unknown = JSON.parse(json);\n return Array.isArray(parsed) ? parsed.filter((entry): entry is string => typeof entry === 'string') : [];\n}\n"]}
@@ -0,0 +1,53 @@
1
+ import type { ReviewOptions, ReviewStore } from './review-types.js';
2
+ /**
3
+ * The half [ADR-0019](../../../docs/context/adr/0019-one-comment-that-leads-with-causes.md) put out of
4
+ * scope: somewhere a change can be looked at, and somewhere a decision about it
5
+ * survives.
6
+ *
7
+ * ## What a build is
8
+ *
9
+ * A `RunReport` and the images that run kept. Nothing here invents a format — the
10
+ * report is what `variance run` already writes, and the images are what it
11
+ * already saved beside it. A CI job posts one thing it already has.
12
+ *
13
+ * ## Approval promotes; it never records
14
+ *
15
+ * `variance accept` "never produces an image — it promotes one the run already
16
+ * produced", and that rule is the reason this file cannot be a simple flag on a
17
+ * row. A baseline is not pixels: it is pixels **plus** the document digest they
18
+ * were painted from and the identity that painted them, which is the sidecar the
19
+ * cheap `describe` path answers from without moving a byte. So a build carries
20
+ * that metadata per subject, and approving writes it through the same
21
+ * `RasterStore` a run reads.
22
+ *
23
+ * A subject whose candidate was never uploaded therefore **cannot be approved**.
24
+ * The alternative is a surface that re-renders in order to say yes, and a surface
25
+ * that can render can record something nobody looked at.
26
+ *
27
+ * ## Two things that are never collapsed
28
+ *
29
+ * **A coverage list that was never stated is not an empty one.** `RunReport`
30
+ * distinguishes them because a run that planned 300 subjects, failed on 50 and
31
+ * found 250 unchanged produces a report in which every observation is clean.
32
+ * Ingest stores which of the two arrived, and every summary below carries it, so
33
+ * "nothing to review" cannot be printed on the authority of a writer that never
34
+ * said what it skipped.
35
+ *
36
+ * **Findings that were never collected are not an absence of defects.** Same
37
+ * distinction, same reason: `[]` means this render was inspected and was clean,
38
+ * and `undefined` means nothing looked.
39
+ *
40
+ * ## What is next door
41
+ *
42
+ * The shapes are in [`review-types.ts`](./review-types.ts) so that the Worker and
43
+ * the React surface can name them without a D1 binding; the two writes that reach
44
+ * the bucket are in [`review-write.ts`](./review-write.ts); writing a report in is
45
+ * [`review-ingest.ts`](./review-ingest.ts) and reading a build back out is
46
+ * [`review-read.ts`](./review-read.ts). What is left here is the store
47
+ * itself — the order the writes happen in, and what each operation refuses.
48
+ */
49
+ export type { BuildDetail, BuildIngest, BuildSummary, CandidateImage, Cause, Coverage, Decision, DecisionRecord, ReviewOptions, ReviewStore, SubjectImages, SubjectView, SweepReport, } from './review-types.js';
50
+ export { ReviewError } from './review-rows.js';
51
+ export type { ChangelogChange, ChangelogRow, TribunalChangelog, TribunalChangelogQuery, } from './changelog.js';
52
+ export declare function createReviewStore(options: ReviewOptions): ReviewStore;
53
+ //# sourceMappingURL=review.d.ts.map