@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,47 @@
1
+ /**
2
+ * One report, and the build a run would have posted from it.
3
+ *
4
+ * Shared by the files that test [`review.ts`](./review.ts) rather than copied
5
+ * into each, because the point of every one of them is what the store does with
6
+ * *this* report — the same three subjects, the same 86 cause pixels against 511
7
+ * collateral. A second copy that drifted by one pixel would turn an ordering
8
+ * claim into two claims about two reports.
9
+ */
10
+ import type { RenderIdentity } from '@variance-authority/core';
11
+ import type { RunReport, VariationRecord } from '@variance-authority/report';
12
+ import { type BuildIngest, type ReviewStore } from './review.js';
13
+ import { type MemoryR2, type SqliteD1 } from './testing.js';
14
+ export declare const IDENTITY: RenderIdentity;
15
+ export declare const CANDIDATE: string;
16
+ export declare const PREVIOUS: string;
17
+ export declare function image(colour: readonly [number, number, number]): string;
18
+ /**
19
+ * A report as `variance run` writes one: two subjects changed by one edit, where
20
+ * the reflowed container carries six times the pixels of the component that was
21
+ * actually edited.
22
+ */
23
+ export declare function report(overrides?: Partial<RunReport>): RunReport;
24
+ export declare function ingest(overrides?: Partial<BuildIngest>): BuildIngest;
25
+ /**
26
+ * The three states a variation arrives in, in one report.
27
+ *
28
+ * Together they are the distinction the table keeps a nullable column for: an arm
29
+ * that was measured and differs, one that was measured and reaches nothing, and
30
+ * one nothing could measure because the parent it named is not in this run.
31
+ */
32
+ export declare const VARIATIONS: readonly VariationRecord[];
33
+ /** The hour a build in these fixtures was posted at. */
34
+ export declare const POSTED = "2026-06-01T12:00:00.000Z";
35
+ /**
36
+ * A store over an empty database and an empty bucket.
37
+ *
38
+ * `now` is a getter rather than a date because the tests that reach retention
39
+ * move the clock after the build was written, which is the only way to have a
40
+ * build that is old without waiting for it to become one.
41
+ */
42
+ export declare function openReview(now?: () => Date): Promise<{
43
+ db: SqliteD1;
44
+ bucket: MemoryR2;
45
+ review: ReviewStore;
46
+ }>;
47
+ //# sourceMappingURL=review.fixtures.d.ts.map
@@ -0,0 +1,145 @@
1
+ /**
2
+ * One report, and the build a run would have posted from it.
3
+ *
4
+ * Shared by the files that test [`review.ts`](./review.ts) rather than copied
5
+ * into each, because the point of every one of them is what the store does with
6
+ * *this* report — the same three subjects, the same 86 cause pixels against 511
7
+ * collateral. A second copy that drifted by one pixel would turn an ordering
8
+ * claim into two claims about two reports.
9
+ */
10
+ import { PNG } from 'pngjs';
11
+ import { createReviewStore } from './review.js';
12
+ import { createMemoryR2, createSqliteD1 } from './testing.js';
13
+ export const IDENTITY = {
14
+ renderer: 'playwright-chromium',
15
+ engine: 'chromium@131.0.0',
16
+ platform: 'linux/x64',
17
+ deviceScaleFactor: 1,
18
+ fonts: [],
19
+ };
20
+ export const CANDIDATE = image([0, 0, 0]);
21
+ export const PREVIOUS = image([255, 255, 255]);
22
+ export function image(colour) {
23
+ const png = new PNG({ width: 2, height: 2 });
24
+ for (let index = 0; index < png.data.length; index += 4) {
25
+ png.data[index] = colour[0];
26
+ png.data[index + 1] = colour[1];
27
+ png.data[index + 2] = colour[2];
28
+ png.data[index + 3] = 255;
29
+ }
30
+ return PNG.sync.write(png).toString('base64');
31
+ }
32
+ /**
33
+ * A report as `variance run` writes one: two subjects changed by one edit, where
34
+ * the reflowed container carries six times the pixels of the component that was
35
+ * actually edited.
36
+ */
37
+ export function report(overrides = {}) {
38
+ return {
39
+ runVersion: 1,
40
+ at: '2026-06-01T10:00:00.000Z',
41
+ identity: IDENTITY,
42
+ retention: 'durable',
43
+ intent: 'tighten the toolbar',
44
+ observations: [
45
+ {
46
+ subject: 'story:todos--populated',
47
+ verdict: 'changed',
48
+ because: 'the rendered image differs from the baseline',
49
+ changedPixels: 1530,
50
+ regions: [
51
+ {
52
+ x: 0, y: 0, width: 10, height: 10, pixels: 86,
53
+ component: 'Toggle', file: 'src/ds/components.tsx', cause: true,
54
+ },
55
+ {
56
+ x: 0, y: 20, width: 40, height: 20, pixels: 511,
57
+ component: 'Stack', file: 'src/ds/components.tsx', cause: false,
58
+ },
59
+ ],
60
+ findings: [],
61
+ },
62
+ {
63
+ subject: 'story:toolbar',
64
+ verdict: 'unchanged',
65
+ because: 'the document digests to what the baseline was painted from',
66
+ changedPixels: 0,
67
+ regions: [],
68
+ },
69
+ ],
70
+ notObserved: [
71
+ { subject: 'story:modal', kind: 'failed', because: 'the renderer crashed on navigation' },
72
+ { subject: 'story:legacy', kind: 'excluded', because: 'excluded by config' },
73
+ ],
74
+ ...overrides,
75
+ };
76
+ }
77
+ export function ingest(overrides = {}) {
78
+ return {
79
+ build: 'ci-1001',
80
+ commit: 'abc123',
81
+ branch: 'feat/toolbar',
82
+ report: report(),
83
+ images: {
84
+ 'story:todos--populated': {
85
+ after: {
86
+ bytes: CANDIDATE,
87
+ documentDigest: 'deadbeef',
88
+ width: 2,
89
+ height: 2,
90
+ missingFonts: [],
91
+ },
92
+ before: { bytes: PREVIOUS },
93
+ diff: { bytes: PREVIOUS },
94
+ },
95
+ },
96
+ ...overrides,
97
+ };
98
+ }
99
+ /**
100
+ * The three states a variation arrives in, in one report.
101
+ *
102
+ * Together they are the distinction the table keeps a nullable column for: an arm
103
+ * that was measured and differs, one that was measured and reaches nothing, and
104
+ * one nothing could measure because the parent it named is not in this run.
105
+ */
106
+ export const VARIATIONS = [
107
+ {
108
+ subject: 'story:todos--populated-dark',
109
+ parent: 'story:todos--populated',
110
+ identical: false,
111
+ bands: ['token'],
112
+ components: ['Toggle'],
113
+ digest: 'v1:d2eebe6199661536',
114
+ how: 'named',
115
+ because: '`story:todos--populated-dark` differs from `story:todos--populated` in token',
116
+ },
117
+ {
118
+ subject: 'story:todos--sale',
119
+ parent: 'story:todos--populated',
120
+ identical: true,
121
+ bands: [],
122
+ digest: 'v1:ce4228e7c7dcb46f',
123
+ how: 'declared',
124
+ because: '`story:todos--sale` renders identically to `story:todos--populated`',
125
+ },
126
+ {
127
+ subject: 'story:todos--orphan',
128
+ because: 'the parent this subject declares was not observed in this run',
129
+ },
130
+ ];
131
+ /** The hour a build in these fixtures was posted at. */
132
+ export const POSTED = '2026-06-01T12:00:00.000Z';
133
+ /**
134
+ * A store over an empty database and an empty bucket.
135
+ *
136
+ * `now` is a getter rather than a date because the tests that reach retention
137
+ * move the clock after the build was written, which is the only way to have a
138
+ * build that is old without waiting for it to become one.
139
+ */
140
+ export async function openReview(now = () => new Date(POSTED)) {
141
+ const db = await createSqliteD1();
142
+ const bucket = createMemoryR2();
143
+ return { db, bucket, review: createReviewStore({ db, bucket, project: 'todomvc', now }) };
144
+ }
145
+ //# sourceMappingURL=review.fixtures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.fixtures.js","sourceRoot":"","sources":["../src/review.fixtures.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One report, and the build a run would have posted from it.\n *\n * Shared by the files that test [`review.ts`](./review.ts) rather than copied\n * into each, because the point of every one of them is what the store does with\n * *this* report — the same three subjects, the same 86 cause pixels against 511\n * collateral. A second copy that drifted by one pixel would turn an ordering\n * claim into two claims about two reports.\n */\n\nimport { PNG } from 'pngjs';\nimport type { Digest, RenderIdentity } from '@variance-authority/core';\nimport type { RunReport, VariationRecord } from '@variance-authority/report';\nimport { createReviewStore, type BuildIngest, type ReviewStore } from './review.js';\nimport { createMemoryR2, createSqliteD1, type MemoryR2, type SqliteD1 } from './testing.js';\n\nexport const IDENTITY: RenderIdentity = {\n renderer: 'playwright-chromium',\n engine: 'chromium@131.0.0',\n platform: 'linux/x64',\n deviceScaleFactor: 1,\n fonts: [],\n};\n\nexport const CANDIDATE = image([0, 0, 0]);\nexport const PREVIOUS = image([255, 255, 255]);\n\nexport function image(colour: readonly [number, number, number]): string {\n const png = new PNG({ width: 2, height: 2 });\n for (let index = 0; index < png.data.length; index += 4) {\n png.data[index] = colour[0];\n png.data[index + 1] = colour[1];\n png.data[index + 2] = colour[2];\n png.data[index + 3] = 255;\n }\n return PNG.sync.write(png).toString('base64');\n}\n\n/**\n * A report as `variance run` writes one: two subjects changed by one edit, where\n * the reflowed container carries six times the pixels of the component that was\n * actually edited.\n */\nexport function report(overrides: Partial<RunReport> = {}): RunReport {\n return {\n runVersion: 1,\n at: '2026-06-01T10:00:00.000Z',\n identity: IDENTITY,\n retention: 'durable',\n intent: 'tighten the toolbar',\n observations: [\n {\n subject: 'story:todos--populated',\n verdict: 'changed',\n because: 'the rendered image differs from the baseline',\n changedPixels: 1530,\n regions: [\n {\n x: 0, y: 0, width: 10, height: 10, pixels: 86,\n component: 'Toggle', file: 'src/ds/components.tsx', cause: true,\n },\n {\n x: 0, y: 20, width: 40, height: 20, pixels: 511,\n component: 'Stack', file: 'src/ds/components.tsx', cause: false,\n },\n ],\n findings: [],\n },\n {\n subject: 'story:toolbar',\n verdict: 'unchanged',\n because: 'the document digests to what the baseline was painted from',\n changedPixels: 0,\n regions: [],\n },\n ],\n notObserved: [\n { subject: 'story:modal', kind: 'failed', because: 'the renderer crashed on navigation' },\n { subject: 'story:legacy', kind: 'excluded', because: 'excluded by config' },\n ],\n ...overrides,\n };\n}\n\nexport function ingest(overrides: Partial<BuildIngest> = {}): BuildIngest {\n return {\n build: 'ci-1001',\n commit: 'abc123',\n branch: 'feat/toolbar',\n report: report(),\n images: {\n 'story:todos--populated': {\n after: {\n bytes: CANDIDATE,\n documentDigest: 'deadbeef' as Digest,\n width: 2,\n height: 2,\n missingFonts: [],\n },\n before: { bytes: PREVIOUS },\n diff: { bytes: PREVIOUS },\n },\n },\n ...overrides,\n };\n}\n\n/**\n * The three states a variation arrives in, in one report.\n *\n * Together they are the distinction the table keeps a nullable column for: an arm\n * that was measured and differs, one that was measured and reaches nothing, and\n * one nothing could measure because the parent it named is not in this run.\n */\nexport const VARIATIONS: readonly VariationRecord[] = [\n {\n subject: 'story:todos--populated-dark',\n parent: 'story:todos--populated',\n identical: false,\n bands: ['token'],\n components: ['Toggle'],\n digest: 'v1:d2eebe6199661536',\n how: 'named',\n because: '`story:todos--populated-dark` differs from `story:todos--populated` in token',\n },\n {\n subject: 'story:todos--sale',\n parent: 'story:todos--populated',\n identical: true,\n bands: [],\n digest: 'v1:ce4228e7c7dcb46f',\n how: 'declared',\n because: '`story:todos--sale` renders identically to `story:todos--populated`',\n },\n {\n subject: 'story:todos--orphan',\n because: 'the parent this subject declares was not observed in this run',\n },\n];\n\n/** The hour a build in these fixtures was posted at. */\nexport const POSTED = '2026-06-01T12:00:00.000Z';\n\n/**\n * A store over an empty database and an empty bucket.\n *\n * `now` is a getter rather than a date because the tests that reach retention\n * move the clock after the build was written, which is the only way to have a\n * build that is old without waiting for it to become one.\n */\nexport async function openReview(\n now: () => Date = () => new Date(POSTED),\n): Promise<{ db: SqliteD1; bucket: MemoryR2; review: ReviewStore }> {\n const db = await createSqliteD1();\n const bucket = createMemoryR2();\n return { db, bucket, review: createReviewStore({ db, bucket, project: 'todomvc', now }) };\n}\n"]}
package/dist/review.js ADDED
@@ -0,0 +1,245 @@
1
+ import { readChangelog, recordApproval } from './changelog.js';
2
+ import { docket, latestDecisions, summarize, toDeclarations, toNotObserved, toMovement, toPlacement, toReach, toSubjectView, toVariation, } from './review-read.js';
3
+ import { ingestBuild } from './review-ingest.js';
4
+ import { ReviewError, instant, number, optionalText, text } from './review-rows.js';
5
+ import { promote } from './review-write.js';
6
+ import { createBucketStore } from './store.js';
7
+ export { ReviewError } from './review-rows.js';
8
+ export function createReviewStore(options) {
9
+ const { db, bucket, project } = options;
10
+ const now = options.now ?? (() => new Date());
11
+ const baselines = createBucketStore(options);
12
+ return {
13
+ async ingest(build) {
14
+ await ingestBuild({ db, bucket, project }, build);
15
+ },
16
+ async builds(limit = 50) {
17
+ const listed = await db
18
+ .prepare('SELECT * FROM builds WHERE project = ? ORDER BY at_ms DESC, rowid DESC LIMIT ?')
19
+ .bind(project, limit)
20
+ .all();
21
+ const summaries = [];
22
+ for (const row of listed.results) {
23
+ summaries.push(await summarize(db, project, row));
24
+ }
25
+ return summaries;
26
+ },
27
+ async build(id) {
28
+ const row = await db
29
+ .prepare('SELECT * FROM builds WHERE project = ? AND build = ?')
30
+ .bind(project, id)
31
+ .first();
32
+ if (row === null)
33
+ return null;
34
+ const summary = await summarize(db, project, row);
35
+ const subjectRows = await db
36
+ .prepare('SELECT * FROM build_subjects WHERE project = ? AND build = ? ORDER BY subject')
37
+ .bind(project, id)
38
+ .all();
39
+ const decisions = await latestDecisions(db, project, id);
40
+ const skipped = await db
41
+ .prepare('SELECT * FROM build_not_observed WHERE project = ? AND build = ? ORDER BY subject')
42
+ .bind(project, id)
43
+ .all();
44
+ const variations = await db
45
+ .prepare('SELECT * FROM build_variations WHERE project = ? AND build = ? ORDER BY subject')
46
+ .bind(project, id)
47
+ .all();
48
+ const reachRow = await db
49
+ .prepare('SELECT * FROM build_reach WHERE project = ? AND build = ?')
50
+ .bind(project, id)
51
+ .first();
52
+ const census = await db
53
+ .prepare('SELECT * FROM build_composition WHERE project = ? AND build = ? ORDER BY component')
54
+ .bind(project, id)
55
+ .all();
56
+ const attributed = await db
57
+ .prepare(`SELECT * FROM build_movements WHERE project = ? AND build = ?
58
+ ORDER BY component, subject`)
59
+ .bind(project, id)
60
+ .all();
61
+ const reachSubjects = reachRow === null
62
+ ? undefined
63
+ : await db
64
+ .prepare('SELECT * FROM build_reach_subjects WHERE project = ? AND build = ? ORDER BY subject')
65
+ .bind(project, id)
66
+ .all();
67
+ const subjects = subjectRows.results.map((subject) => toSubjectView(subject, decisions.get(text(subject, 'subject', 'a build subject')) ?? null));
68
+ return {
69
+ ...summary,
70
+ subjects,
71
+ notObserved: skipped.results.map(toNotObserved),
72
+ causes: docket(subjects),
73
+ variations: variations.results.map(toVariation),
74
+ reach: reachRow === null ? null : toReach(reachRow, reachSubjects?.results ?? []),
75
+ // No rows is `null`, not `[]`. A run that produced no semantic snapshots
76
+ // has no graph to join, and an empty list would say the opposite — that
77
+ // the suite was read and found to contain no component at all.
78
+ composition: census.results.length === 0 ? null : census.results.map(toPlacement),
79
+ movements: attributed.results.map(toMovement),
80
+ declarations: toDeclarations(row),
81
+ };
82
+ },
83
+ async image(build, subject, kind) {
84
+ const row = await db
85
+ .prepare(`SELECT before_key, after_key, diff_key FROM build_subjects
86
+ WHERE project = ? AND build = ? AND subject = ?`)
87
+ .bind(project, build, subject)
88
+ .first();
89
+ if (row === null)
90
+ return null;
91
+ const key = optionalText(row, `${kind}_key`, 'a build subject');
92
+ if (key === undefined)
93
+ return null;
94
+ const object = await bucket.get(key);
95
+ // An object a row points at and that is not there is damage, not absence —
96
+ // the same rule the baseline store applies. Here it costs a broken image in
97
+ // a page rather than a destroyed baseline, so it is reported rather than
98
+ // fatal, but it is still not answered as "the run kept none".
99
+ if (object === null) {
100
+ throw new ReviewError(`build "${build}" says it kept the ${kind} image for ${subject} at \`${key}\`, and the ` +
101
+ 'bucket has no such object. The row and the object are one artifact; one without the ' +
102
+ 'other is damage rather than a run that saved nothing');
103
+ }
104
+ return object.arrayBuffer();
105
+ },
106
+ async decide(input) {
107
+ const row = await db
108
+ .prepare('SELECT * FROM build_subjects WHERE project = ? AND build = ? AND subject = ?')
109
+ .bind(project, input.build, input.subject)
110
+ .first();
111
+ if (row === null) {
112
+ throw new ReviewError(`build "${input.build}" has no subject "${input.subject}". Deciding about a subject a ` +
113
+ 'build never reported would record an approval nothing can be promoted for');
114
+ }
115
+ const at = now().toISOString();
116
+ // Promotion happens *before* the decision is recorded. The other order can
117
+ // leave an approval on the page whose baseline was never written, and the
118
+ // next run would then report the same change again with the reviewer's name
119
+ // already against it.
120
+ if (input.decision === 'approved') {
121
+ await promote(bucket, baselines, db, project, input.build, input.subject, row);
122
+ // Beside the promotion, and only for one: an approval is the moment a
123
+ // baseline changed, and it is the last moment at which anything still
124
+ // knows what the change was. `build_subjects` expires; this does not.
125
+ await recordApproval(db, project, {
126
+ build: input.build,
127
+ subject: input.subject,
128
+ by: input.by,
129
+ at,
130
+ regions: text(row, 'regions', 'a build subject'),
131
+ ...(input.note !== undefined ? { note: input.note } : {}),
132
+ ...(await buildContext(db, project, input.build)),
133
+ });
134
+ }
135
+ await db
136
+ .prepare(`INSERT INTO decisions (project, build, subject, decision, decided_by, note, at, at_ms)
137
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
138
+ .bind(project, input.build, input.subject, input.decision, input.by, input.note ?? null, at, instant(at, 'a decision'))
139
+ .run();
140
+ return {
141
+ decision: input.decision,
142
+ by: input.by,
143
+ at,
144
+ ...(input.note !== undefined ? { note: input.note } : {}),
145
+ };
146
+ },
147
+ async changelog(query) {
148
+ return readChangelog(db, project, query ?? {});
149
+ },
150
+ async sweep(keepDays) {
151
+ if (!Number.isFinite(keepDays) || keepDays < 0) {
152
+ throw new ReviewError(`a retention window of ${String(keepDays)} days is not a window. Pass the number of ` +
153
+ 'days of builds to keep; 0 keeps none');
154
+ }
155
+ const cutoff = now().getTime() - keepDays * 86_400_000;
156
+ const expired = await db
157
+ .prepare('SELECT build FROM builds WHERE project = ? AND at_ms < ?')
158
+ .bind(project, cutoff)
159
+ .all();
160
+ const ids = expired.results.map((row) => text(row, 'build', 'a build'));
161
+ if (ids.length === 0)
162
+ return { builds: 0, subjects: 0, objects: 0, decisionsKept: 0 };
163
+ let objects = 0;
164
+ let subjects = 0;
165
+ let decisionsKept = 0;
166
+ for (const id of ids) {
167
+ const rows = await db
168
+ .prepare('SELECT before_key, after_key, diff_key FROM build_subjects WHERE project = ? AND build = ?')
169
+ .bind(project, id)
170
+ .all();
171
+ const keys = rows.results.flatMap((row) => ['before_key', 'after_key', 'diff_key']
172
+ .map((column) => optionalText(row, column, 'a build subject'))
173
+ .filter((key) => key !== undefined));
174
+ if (keys.length > 0)
175
+ await bucket.delete(keys);
176
+ objects += keys.length;
177
+ subjects += rows.results.length;
178
+ const counted = await db
179
+ .prepare('SELECT COUNT(*) AS n FROM decisions WHERE project = ? AND build = ?')
180
+ .bind(project, id)
181
+ .first();
182
+ decisionsKept += number(counted ?? {}, 'n', 'a decision count');
183
+ // `decisions` carries a permanence trigger and is deliberately not swept:
184
+ // a promoted baseline whose approval was deleted is a change nobody can
185
+ // attribute to anyone. What expires is what a build kept to be *looked
186
+ // at*, which is images and the verdicts beside them.
187
+ await db
188
+ .prepare('DELETE FROM build_subjects WHERE project = ? AND build = ?')
189
+ .bind(project, id)
190
+ .run();
191
+ await db
192
+ .prepare('DELETE FROM build_not_observed WHERE project = ? AND build = ?')
193
+ .bind(project, id)
194
+ .run();
195
+ await db
196
+ .prepare('DELETE FROM build_variations WHERE project = ? AND build = ?')
197
+ .bind(project, id)
198
+ .run();
199
+ await db
200
+ .prepare('DELETE FROM build_composition WHERE project = ? AND build = ?')
201
+ .bind(project, id)
202
+ .run();
203
+ await db
204
+ .prepare('DELETE FROM build_movements WHERE project = ? AND build = ?')
205
+ .bind(project, id)
206
+ .run();
207
+ await db
208
+ .prepare('DELETE FROM build_reach_subjects WHERE project = ? AND build = ?')
209
+ .bind(project, id)
210
+ .run();
211
+ await db
212
+ .prepare('DELETE FROM build_reach WHERE project = ? AND build = ?')
213
+ .bind(project, id)
214
+ .run();
215
+ await db.prepare('DELETE FROM builds WHERE project = ? AND build = ?').bind(project, id).run();
216
+ }
217
+ return { builds: ids.length, subjects, objects, decisionsKept };
218
+ },
219
+ };
220
+ }
221
+ /**
222
+ * The two things a changelog entry needs from the build and cannot invent.
223
+ *
224
+ * Read at approval time rather than joined at read time, because the build row
225
+ * is what `sweep` removes. A build that has already gone is not a failure here —
226
+ * its subjects would have gone with it, so there would be nothing to approve —
227
+ * but the read is defended anyway, since an entry attributing a baseline to an
228
+ * empty commit is worse than one that says nothing.
229
+ */
230
+ async function buildContext(db, project, build) {
231
+ const row = await db
232
+ .prepare('SELECT "commit", intent FROM builds WHERE project = ? AND build = ?')
233
+ .bind(project, build)
234
+ .first();
235
+ if (row === null) {
236
+ throw new ReviewError(`build "${build}" is no longer in this database, so an approval under it could not be ` +
237
+ 'attributed to a commit. A changelog entry naming no commit explains nothing');
238
+ }
239
+ const intent = optionalText(row, 'intent', 'a build');
240
+ return {
241
+ commit: text(row, 'commit', 'a build'),
242
+ ...(intent !== undefined ? { intent } : {}),
243
+ };
244
+ }
245
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../src/review.ts"],"names":[],"mappings":"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","sourcesContent":["import type { D1Like } from './bindings.js';\nimport { readChangelog, recordApproval } from './changelog.js';\nimport {\n docket,\n latestDecisions,\n summarize,\n toDeclarations,\n toNotObserved,\n toMovement,\n toPlacement,\n toReach,\n toSubjectView,\n toVariation,\n} from './review-read.js';\nimport { ingestBuild } from './review-ingest.js';\nimport { ReviewError, instant, number, optionalText, text, type Row } from './review-rows.js';\nimport type {\n BuildDetail,\n BuildSummary,\n DecisionRecord,\n ReviewOptions,\n ReviewStore,\n SweepReport,\n} from './review-types.js';\nimport { promote } from './review-write.js';\nimport type { TribunalChangelog } from './changelog.js';\nimport { createBucketStore } from './store.js';\n\n/**\n * The half [ADR-0019](../../../docs/context/adr/0019-one-comment-that-leads-with-causes.md) put out of\n * scope: somewhere a change can be looked at, and somewhere a decision about it\n * survives.\n *\n * ## What a build is\n *\n * A `RunReport` and the images that run kept. Nothing here invents a format — the\n * report is what `variance run` already writes, and the images are what it\n * already saved beside it. A CI job posts one thing it already has.\n *\n * ## Approval promotes; it never records\n *\n * `variance accept` \"never produces an image — it promotes one the run already\n * produced\", and that rule is the reason this file cannot be a simple flag on a\n * row. A baseline is not pixels: it is pixels **plus** the document digest they\n * were painted from and the identity that painted them, which is the sidecar the\n * cheap `describe` path answers from without moving a byte. So a build carries\n * that metadata per subject, and approving writes it through the same\n * `RasterStore` a run reads.\n *\n * A subject whose candidate was never uploaded therefore **cannot be approved**.\n * The alternative is a surface that re-renders in order to say yes, and a surface\n * that can render can record something nobody looked at.\n *\n * ## Two things that are never collapsed\n *\n * **A coverage list that was never stated is not an empty one.** `RunReport`\n * distinguishes them because a run that planned 300 subjects, failed on 50 and\n * found 250 unchanged produces a report in which every observation is clean.\n * Ingest stores which of the two arrived, and every summary below carries it, so\n * \"nothing to review\" cannot be printed on the authority of a writer that never\n * said what it skipped.\n *\n * **Findings that were never collected are not an absence of defects.** Same\n * distinction, same reason: `[]` means this render was inspected and was clean,\n * and `undefined` means nothing looked.\n *\n * ## What is next door\n *\n * The shapes are in [`review-types.ts`](./review-types.ts) so that the Worker and\n * the React surface can name them without a D1 binding; the two writes that reach\n * the bucket are in [`review-write.ts`](./review-write.ts); writing a report in is\n * [`review-ingest.ts`](./review-ingest.ts) and reading a build back out is\n * [`review-read.ts`](./review-read.ts). What is left here is the store\n * itself — the order the writes happen in, and what each operation refuses.\n */\n\nexport type {\n BuildDetail,\n BuildIngest,\n BuildSummary,\n CandidateImage,\n Cause,\n Coverage,\n Decision,\n DecisionRecord,\n ReviewOptions,\n ReviewStore,\n SubjectImages,\n SubjectView,\n SweepReport,\n} from './review-types.js';\nexport { ReviewError } from './review-rows.js';\nexport type {\n ChangelogChange,\n ChangelogRow,\n TribunalChangelog,\n TribunalChangelogQuery,\n} from './changelog.js';\n\nexport function createReviewStore(options: ReviewOptions): ReviewStore {\n const { db, bucket, project } = options;\n const now = options.now ?? ((): Date => new Date());\n const baselines = createBucketStore(options);\n\n return {\n async ingest(build): Promise<void> {\n await ingestBuild({ db, bucket, project }, build);\n },\n\n async builds(limit = 50): Promise<readonly BuildSummary[]> {\n const listed = await db\n .prepare('SELECT * FROM builds WHERE project = ? ORDER BY at_ms DESC, rowid DESC LIMIT ?')\n .bind(project, limit)\n .all<Row>();\n\n const summaries: BuildSummary[] = [];\n for (const row of listed.results) {\n summaries.push(await summarize(db, project, row));\n }\n return summaries;\n },\n\n async build(id): Promise<BuildDetail | null> {\n const row = await db\n .prepare('SELECT * FROM builds WHERE project = ? AND build = ?')\n .bind(project, id)\n .first<Row>();\n if (row === null) return null;\n\n const summary = await summarize(db, project, row);\n const subjectRows = await db\n .prepare('SELECT * FROM build_subjects WHERE project = ? AND build = ? ORDER BY subject')\n .bind(project, id)\n .all<Row>();\n const decisions = await latestDecisions(db, project, id);\n const skipped = await db\n .prepare('SELECT * FROM build_not_observed WHERE project = ? AND build = ? ORDER BY subject')\n .bind(project, id)\n .all<Row>();\n const variations = await db\n .prepare('SELECT * FROM build_variations WHERE project = ? AND build = ? ORDER BY subject')\n .bind(project, id)\n .all<Row>();\n const reachRow = await db\n .prepare('SELECT * FROM build_reach WHERE project = ? AND build = ?')\n .bind(project, id)\n .first<Row>();\n const census = await db\n .prepare(\n 'SELECT * FROM build_composition WHERE project = ? AND build = ? ORDER BY component',\n )\n .bind(project, id)\n .all<Row>();\n const attributed = await db\n .prepare(\n `SELECT * FROM build_movements WHERE project = ? AND build = ?\n ORDER BY component, subject`,\n )\n .bind(project, id)\n .all<Row>();\n const reachSubjects =\n reachRow === null\n ? undefined\n : await db\n .prepare(\n 'SELECT * FROM build_reach_subjects WHERE project = ? AND build = ? ORDER BY subject',\n )\n .bind(project, id)\n .all<Row>();\n\n const subjects = subjectRows.results.map((subject) =>\n toSubjectView(subject, decisions.get(text(subject, 'subject', 'a build subject')) ?? null),\n );\n\n return {\n ...summary,\n subjects,\n notObserved: skipped.results.map(toNotObserved),\n causes: docket(subjects),\n variations: variations.results.map(toVariation),\n reach: reachRow === null ? null : toReach(reachRow, reachSubjects?.results ?? []),\n // No rows is `null`, not `[]`. A run that produced no semantic snapshots\n // has no graph to join, and an empty list would say the opposite — that\n // the suite was read and found to contain no component at all.\n composition: census.results.length === 0 ? null : census.results.map(toPlacement),\n movements: attributed.results.map(toMovement),\n declarations: toDeclarations(row),\n };\n },\n\n async image(build, subject, kind): Promise<ArrayBuffer | null> {\n const row = await db\n .prepare(\n `SELECT before_key, after_key, diff_key FROM build_subjects\n WHERE project = ? AND build = ? AND subject = ?`,\n )\n .bind(project, build, subject)\n .first<Row>();\n if (row === null) return null;\n\n const key = optionalText(row, `${kind}_key`, 'a build subject');\n if (key === undefined) return null;\n\n const object = await bucket.get(key);\n // An object a row points at and that is not there is damage, not absence —\n // the same rule the baseline store applies. Here it costs a broken image in\n // a page rather than a destroyed baseline, so it is reported rather than\n // fatal, but it is still not answered as \"the run kept none\".\n if (object === null) {\n throw new ReviewError(\n `build \"${build}\" says it kept the ${kind} image for ${subject} at \\`${key}\\`, and the ` +\n 'bucket has no such object. The row and the object are one artifact; one without the ' +\n 'other is damage rather than a run that saved nothing',\n );\n }\n return object.arrayBuffer();\n },\n\n async decide(input): Promise<DecisionRecord> {\n const row = await db\n .prepare('SELECT * FROM build_subjects WHERE project = ? AND build = ? AND subject = ?')\n .bind(project, input.build, input.subject)\n .first<Row>();\n\n if (row === null) {\n throw new ReviewError(\n `build \"${input.build}\" has no subject \"${input.subject}\". Deciding about a subject a ` +\n 'build never reported would record an approval nothing can be promoted for',\n );\n }\n\n const at = now().toISOString();\n\n // Promotion happens *before* the decision is recorded. The other order can\n // leave an approval on the page whose baseline was never written, and the\n // next run would then report the same change again with the reviewer's name\n // already against it.\n if (input.decision === 'approved') {\n await promote(bucket, baselines, db, project, input.build, input.subject, row);\n // Beside the promotion, and only for one: an approval is the moment a\n // baseline changed, and it is the last moment at which anything still\n // knows what the change was. `build_subjects` expires; this does not.\n await recordApproval(db, project, {\n build: input.build,\n subject: input.subject,\n by: input.by,\n at,\n regions: text(row, 'regions', 'a build subject'),\n ...(input.note !== undefined ? { note: input.note } : {}),\n ...(await buildContext(db, project, input.build)),\n });\n }\n\n await db\n .prepare(\n `INSERT INTO decisions (project, build, subject, decision, decided_by, note, at, at_ms)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .bind(\n project,\n input.build,\n input.subject,\n input.decision,\n input.by,\n input.note ?? null,\n at,\n instant(at, 'a decision'),\n )\n .run();\n\n return {\n decision: input.decision,\n by: input.by,\n at,\n ...(input.note !== undefined ? { note: input.note } : {}),\n };\n },\n\n async changelog(query): Promise<TribunalChangelog> {\n return readChangelog(db, project, query ?? {});\n },\n\n async sweep(keepDays): Promise<SweepReport> {\n if (!Number.isFinite(keepDays) || keepDays < 0) {\n throw new ReviewError(\n `a retention window of ${String(keepDays)} days is not a window. Pass the number of ` +\n 'days of builds to keep; 0 keeps none',\n );\n }\n\n const cutoff = now().getTime() - keepDays * 86_400_000;\n const expired = await db\n .prepare('SELECT build FROM builds WHERE project = ? AND at_ms < ?')\n .bind(project, cutoff)\n .all<Row>();\n\n const ids = expired.results.map((row) => text(row, 'build', 'a build'));\n if (ids.length === 0) return { builds: 0, subjects: 0, objects: 0, decisionsKept: 0 };\n\n let objects = 0;\n let subjects = 0;\n let decisionsKept = 0;\n\n for (const id of ids) {\n const rows = await db\n .prepare('SELECT before_key, after_key, diff_key FROM build_subjects WHERE project = ? AND build = ?')\n .bind(project, id)\n .all<Row>();\n\n const keys = rows.results.flatMap((row) =>\n (['before_key', 'after_key', 'diff_key'] as const)\n .map((column) => optionalText(row, column, 'a build subject'))\n .filter((key): key is string => key !== undefined),\n );\n\n if (keys.length > 0) await bucket.delete(keys);\n objects += keys.length;\n subjects += rows.results.length;\n\n const counted = await db\n .prepare('SELECT COUNT(*) AS n FROM decisions WHERE project = ? AND build = ?')\n .bind(project, id)\n .first<Row>();\n decisionsKept += number(counted ?? {}, 'n', 'a decision count');\n\n // `decisions` carries a permanence trigger and is deliberately not swept:\n // a promoted baseline whose approval was deleted is a change nobody can\n // attribute to anyone. What expires is what a build kept to be *looked\n // at*, which is images and the verdicts beside them.\n await db\n .prepare('DELETE FROM build_subjects WHERE project = ? AND build = ?')\n .bind(project, id)\n .run();\n await db\n .prepare('DELETE FROM build_not_observed WHERE project = ? AND build = ?')\n .bind(project, id)\n .run();\n await db\n .prepare('DELETE FROM build_variations WHERE project = ? AND build = ?')\n .bind(project, id)\n .run();\n await db\n .prepare('DELETE FROM build_composition WHERE project = ? AND build = ?')\n .bind(project, id)\n .run();\n await db\n .prepare('DELETE FROM build_movements WHERE project = ? AND build = ?')\n .bind(project, id)\n .run();\n await db\n .prepare('DELETE FROM build_reach_subjects WHERE project = ? AND build = ?')\n .bind(project, id)\n .run();\n await db\n .prepare('DELETE FROM build_reach WHERE project = ? AND build = ?')\n .bind(project, id)\n .run();\n await db.prepare('DELETE FROM builds WHERE project = ? AND build = ?').bind(project, id).run();\n }\n\n return { builds: ids.length, subjects, objects, decisionsKept };\n },\n };\n}\n\n/**\n * The two things a changelog entry needs from the build and cannot invent.\n *\n * Read at approval time rather than joined at read time, because the build row\n * is what `sweep` removes. A build that has already gone is not a failure here —\n * its subjects would have gone with it, so there would be nothing to approve —\n * but the read is defended anyway, since an entry attributing a baseline to an\n * empty commit is worse than one that says nothing.\n */\nasync function buildContext(\n db: D1Like,\n project: string,\n build: string,\n): Promise<{ readonly commit: string; readonly intent?: string }> {\n const row = await db\n .prepare('SELECT \"commit\", intent FROM builds WHERE project = ? AND build = ?')\n .bind(project, build)\n .first<Row>();\n\n if (row === null) {\n throw new ReviewError(\n `build \"${build}\" is no longer in this database, so an approval under it could not be ` +\n 'attributed to a commit. A changelog entry naming no commit explains nothing',\n );\n }\n\n const intent = optionalText(row, 'intent', 'a build');\n return {\n commit: text(row, 'commit', 'a build'),\n ...(intent !== undefined ? { intent } : {}),\n };\n}\n"]}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The version this deployment is on, and the one call that puts a database there.
3
+ *
4
+ * The statements themselves are in [`migrations.ts`](./migrations.js) and
5
+ * [`migration-steps.ts`](./migration-steps.js) — this is the reading of them:
6
+ * which version they land on, what a fresh database gets, and the batch that
7
+ * applies it. They were one module until the steps outgrew it, and the ten lines
8
+ * at the bottom of this file are the part an operator is usually looking for.
9
+ */
10
+ import type { D1Like } from './bindings.js';
11
+ /** Bumped when the stored shape changes in a way an older build would misread. */
12
+ export declare const SCHEMA_VERSION = 14;
13
+ export { MIGRATIONS } from './migration-steps.js';
14
+ export { INITIAL, INITIAL_VERSION } from './migrations.js';
15
+ /**
16
+ * Every statement, in order — the initial set followed by each step.
17
+ *
18
+ * This is what a fresh database gets, and what the tests run against. The split
19
+ * between them is about *deployment*; nothing downstream of here needs to know a
20
+ * database was built in more than one sitting.
21
+ */
22
+ export declare const SCHEMA: readonly string[];
23
+ /**
24
+ * Create the schema, once, in one batch.
25
+ *
26
+ * Exported for the operator to call from `wrangler d1 execute` equivalents or
27
+ * from a one-off route they mount themselves — **not** called by the Worker on
28
+ * request. A handler that migrates on first use is a handler that migrates
29
+ * concurrently under load, and D1 has no advisory lock to serialize it with.
30
+ *
31
+ * Applying it twice fails on the first `CREATE TABLE`, which is the intended
32
+ * behaviour: an operator who cannot tell whether the schema is applied should get
33
+ * an error rather than a silent no-op that might have half-applied.
34
+ */
35
+ export declare function applySchema(db: D1Like): Promise<void>;
36
+ //# sourceMappingURL=schema.d.ts.map
package/dist/schema.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The version this deployment is on, and the one call that puts a database there.
3
+ *
4
+ * The statements themselves are in [`migrations.ts`](./migrations.js) and
5
+ * [`migration-steps.ts`](./migration-steps.js) — this is the reading of them:
6
+ * which version they land on, what a fresh database gets, and the batch that
7
+ * applies it. They were one module until the steps outgrew it, and the ten lines
8
+ * at the bottom of this file are the part an operator is usually looking for.
9
+ */
10
+ import { MIGRATIONS } from './migration-steps.js';
11
+ import { INITIAL } from './migrations.js';
12
+ /** Bumped when the stored shape changes in a way an older build would misread. */
13
+ export const SCHEMA_VERSION = 14;
14
+ export { MIGRATIONS } from './migration-steps.js';
15
+ export { INITIAL, INITIAL_VERSION } from './migrations.js';
16
+ /**
17
+ * Every statement, in order — the initial set followed by each step.
18
+ *
19
+ * This is what a fresh database gets, and what the tests run against. The split
20
+ * between them is about *deployment*; nothing downstream of here needs to know a
21
+ * database was built in more than one sitting.
22
+ */
23
+ export const SCHEMA = [...INITIAL, ...MIGRATIONS.flat()];
24
+ /**
25
+ * Create the schema, once, in one batch.
26
+ *
27
+ * Exported for the operator to call from `wrangler d1 execute` equivalents or
28
+ * from a one-off route they mount themselves — **not** called by the Worker on
29
+ * request. A handler that migrates on first use is a handler that migrates
30
+ * concurrently under load, and D1 has no advisory lock to serialize it with.
31
+ *
32
+ * Applying it twice fails on the first `CREATE TABLE`, which is the intended
33
+ * behaviour: an operator who cannot tell whether the schema is applied should get
34
+ * an error rather than a silent no-op that might have half-applied.
35
+ */
36
+ export async function applySchema(db) {
37
+ await db.batch(SCHEMA.map((statement) => db.prepare(statement)));
38
+ }
39
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,kFAAkF;AAClF,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAE3D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,MAAM,GAAsB,CAAC,GAAG,OAAO,EAAE,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC;AAE5E;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAAU;IAC1C,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC","sourcesContent":["/**\n * The version this deployment is on, and the one call that puts a database there.\n *\n * The statements themselves are in [`migrations.ts`](./migrations.js) and\n * [`migration-steps.ts`](./migration-steps.js) — this is the reading of them:\n * which version they land on, what a fresh database gets, and the batch that\n * applies it. They were one module until the steps outgrew it, and the ten lines\n * at the bottom of this file are the part an operator is usually looking for.\n */\n\nimport type { D1Like } from './bindings.js';\nimport { MIGRATIONS } from './migration-steps.js';\nimport { INITIAL } from './migrations.js';\n\n/** Bumped when the stored shape changes in a way an older build would misread. */\nexport const SCHEMA_VERSION = 14;\n\nexport { MIGRATIONS } from './migration-steps.js';\nexport { INITIAL, INITIAL_VERSION } from './migrations.js';\n\n/**\n * Every statement, in order — the initial set followed by each step.\n *\n * This is what a fresh database gets, and what the tests run against. The split\n * between them is about *deployment*; nothing downstream of here needs to know a\n * database was built in more than one sitting.\n */\nexport const SCHEMA: readonly string[] = [...INITIAL, ...MIGRATIONS.flat()];\n\n/**\n * Create the schema, once, in one batch.\n *\n * Exported for the operator to call from `wrangler d1 execute` equivalents or\n * from a one-off route they mount themselves — **not** called by the Worker on\n * request. A handler that migrates on first use is a handler that migrates\n * concurrently under load, and D1 has no advisory lock to serialize it with.\n *\n * Applying it twice fails on the first `CREATE TABLE`, which is the intended\n * behaviour: an operator who cannot tell whether the schema is applied should get\n * an error rather than a silent no-op that might have half-applied.\n */\nexport async function applySchema(db: D1Like): Promise<void> {\n await db.batch(SCHEMA.map((statement) => db.prepare(statement)));\n}\n"]}