@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,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.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/__fixtures__/review.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"]}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The platform, named in full, in one file, by us.
3
+ *
4
+ * Every other module here takes a `D1Like` and an `R2Like` and never a
5
+ * Cloudflare type. That is not portability theatre — it is the same rule
6
+ * [ADR-0013](../../../docs/context/adr/0013-packages-are-named-for-their-requirements.md)
7
+ * applies everywhere else in this repository: a package requires what it says it
8
+ * requires. Depending on `@cloudflare/workers-types` would put a global
9
+ * `DOM`-shaped ambient declaration into the build of a package the CLI links
10
+ * against, and it would make the store untestable without a Workers runtime.
11
+ *
12
+ * What is declared below is exactly the surface used, and nothing else. It is
13
+ * structurally satisfied by the real `D1Database` and `R2Bucket` — an operator's
14
+ * Worker passes `env.DB` and `env.BUCKET` straight in and TypeScript accepts it,
15
+ * because a wider interface is assignable to a narrower one.
16
+ *
17
+ * **The cost is stated rather than hidden.** These are hand-written shapes, so a
18
+ * breaking change to D1 or R2 is a runtime failure here rather than a compile
19
+ * error. The mitigation is that the surface is tiny: five methods on D1, three on
20
+ * R2, all of them years old and all of them exercised by the tests through a
21
+ * `node:sqlite`-backed double that runs the same SQL a real D1 would.
22
+ */
23
+ /** A value D1 accepts as a bound parameter. D1 has no `bigint` and no `Buffer`. */
24
+ export type D1Value = string | number | null | ArrayBuffer;
25
+ export interface D1PreparedLike {
26
+ bind(...values: readonly D1Value[]): D1PreparedLike;
27
+ first<Row>(): Promise<Row | null>;
28
+ all<Row>(): Promise<{
29
+ readonly results: readonly Row[];
30
+ }>;
31
+ run(): Promise<unknown>;
32
+ }
33
+ /**
34
+ * A SQL database that runs one statement or a batch of them.
35
+ *
36
+ * D1 is SQLite, so this is satisfied by the platform binding and by
37
+ * `node:sqlite` through a thin wrapper, which is what lets the same store back
38
+ * a Worker and a file on a machine somebody owns.
39
+ */
40
+ export interface D1Like {
41
+ prepare(sql: string): D1PreparedLike;
42
+ /**
43
+ * Several statements, committed together or not at all.
44
+ *
45
+ * The whole reason the history backend can satisfy its atomicity requirement
46
+ * on a platform with no `BEGIN`. D1 wraps a batch in an implicit transaction
47
+ * and rolls the whole thing back if any statement fails, which is what
48
+ * [spec 0002](../../../docs/specs/0002-history-store.md) needs: rows without
49
+ * their run leave a change with no denominator, and a run without its rows is
50
+ * a quiet run that was not quiet.
51
+ *
52
+ * **Unverified against the platform.** See the known limits in
53
+ * [ADR-0023](../../../docs/context/adr/0023-a-service-is-named-for-what-it-is.md) — the
54
+ * double used in tests is genuinely transactional, and the claim that D1 is
55
+ * has never been measured here.
56
+ */
57
+ batch(statements: readonly D1PreparedLike[]): Promise<unknown>;
58
+ }
59
+ export interface R2ObjectLike {
60
+ arrayBuffer(): Promise<ArrayBuffer>;
61
+ }
62
+ /**
63
+ * An object store addressed by key, holding image bytes and nothing else.
64
+ *
65
+ * Four methods, because that is all the store uses: read, exists, write, remove.
66
+ * A bucket is never listed and never scanned — every key this package reads is
67
+ * one a row already named.
68
+ */
69
+ export interface R2Like {
70
+ /** `null` means the bucket answered and there is no such object. */
71
+ get(key: string): Promise<R2ObjectLike | null>;
72
+ /**
73
+ * Existence without the bytes.
74
+ *
75
+ * Here for one reason: `describe` must agree with `find` about whether a
76
+ * baseline exists. A sidecar row whose object is gone would be a description to
77
+ * the cheap lookup and a corrupted pair to the full one, and the verdict would
78
+ * then depend on which question the caller happened to ask. The directory store
79
+ * spends a `stat` for exactly this; this spends a HEAD.
80
+ */
81
+ head(key: string): Promise<unknown | null>;
82
+ put(key: string, value: ArrayBuffer): Promise<unknown>;
83
+ delete(keys: string | readonly string[]): Promise<unknown>;
84
+ }
85
+ /**
86
+ * The two bindings, together, because nothing here works with one of them.
87
+ *
88
+ * A deployment with a database and no bucket can record that a subject changed
89
+ * and cannot show anyone what it looks like; a deployment with a bucket and no
90
+ * database has images nobody can address. Taking them as a pair means the
91
+ * missing one is a wiring error at construction rather than a `TypeError` on the
92
+ * first request that happens to need it.
93
+ */
94
+ export interface TribunalBindings {
95
+ readonly db: D1Like;
96
+ readonly bucket: R2Like;
97
+ }
98
+ /**
99
+ * Base64 in, `ArrayBuffer` out — the one conversion this package does twice.
100
+ *
101
+ * A `Raster` carries its bytes as base64 precisely so it survives JSON hops, and
102
+ * R2 stores bytes. `atob` is the conversion both Workers and Node 22 have; going
103
+ * through `Buffer` would tie this file to Node and would be the only thing in the
104
+ * package that could not run on the platform it is named for.
105
+ */
106
+ export declare function bytesOf(base64: string): ArrayBuffer;
107
+ /** The other direction. Chunked, because `String.fromCharCode` has an argument limit. */
108
+ export declare function base64Of(buffer: ArrayBuffer): string;
109
+ /**
110
+ * Refuse a deployment whose bindings never arrived.
111
+ *
112
+ * The operator who gets this wrong wrote a `wrangler.jsonc` — spelled the
113
+ * binding `D1` instead of `DB`, or added the R2 bucket to production and not to
114
+ * a preview environment — and the compiler is not standing there. Without this,
115
+ * `undefined` is carried all the way to the first statement that touches it and
116
+ * surfaces as `Cannot read properties of undefined (reading 'prepare')` on one
117
+ * request, while routes that happen not to reach the missing half keep working.
118
+ *
119
+ * `worker-entry` already turns every construction refusal into a 500 whose body
120
+ * is the sentence, and it already falls back for `PROJECT` and `RETENTION_DAYS`
121
+ * rather than letting them fail late. The two bindings are the settings most
122
+ * likely to be wrong and were the only ones with no sentence.
123
+ *
124
+ * Shape-checked rather than merely present, because a binding pointing at the
125
+ * wrong kind of resource is the same mistake one line further along, and
126
+ * `R2Like` and `D1Like` share no method name for the two to be confused by.
127
+ * Split in two because `./history` takes a database and no bucket, and one
128
+ * function with an optional second argument cannot tell that door from a store
129
+ * whose bucket is missing.
130
+ */
131
+ export declare function requireD1(db: unknown): void;
132
+ /** The other half. See {@link requireD1}. */
133
+ export declare function requireR2(bucket: unknown): void;
134
+ //# sourceMappingURL=bindings.d.ts.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The platform, named in full, in one file, by us.
3
+ *
4
+ * Every other module here takes a `D1Like` and an `R2Like` and never a
5
+ * Cloudflare type. That is not portability theatre — it is the same rule
6
+ * [ADR-0013](../../../docs/context/adr/0013-packages-are-named-for-their-requirements.md)
7
+ * applies everywhere else in this repository: a package requires what it says it
8
+ * requires. Depending on `@cloudflare/workers-types` would put a global
9
+ * `DOM`-shaped ambient declaration into the build of a package the CLI links
10
+ * against, and it would make the store untestable without a Workers runtime.
11
+ *
12
+ * What is declared below is exactly the surface used, and nothing else. It is
13
+ * structurally satisfied by the real `D1Database` and `R2Bucket` — an operator's
14
+ * Worker passes `env.DB` and `env.BUCKET` straight in and TypeScript accepts it,
15
+ * because a wider interface is assignable to a narrower one.
16
+ *
17
+ * **The cost is stated rather than hidden.** These are hand-written shapes, so a
18
+ * breaking change to D1 or R2 is a runtime failure here rather than a compile
19
+ * error. The mitigation is that the surface is tiny: five methods on D1, three on
20
+ * R2, all of them years old and all of them exercised by the tests through a
21
+ * `node:sqlite`-backed double that runs the same SQL a real D1 would.
22
+ */
23
+ /**
24
+ * Base64 in, `ArrayBuffer` out — the one conversion this package does twice.
25
+ *
26
+ * A `Raster` carries its bytes as base64 precisely so it survives JSON hops, and
27
+ * R2 stores bytes. `atob` is the conversion both Workers and Node 22 have; going
28
+ * through `Buffer` would tie this file to Node and would be the only thing in the
29
+ * package that could not run on the platform it is named for.
30
+ */
31
+ export function bytesOf(base64) {
32
+ const binary = atob(base64);
33
+ const bytes = new Uint8Array(binary.length);
34
+ for (let index = 0; index < binary.length; index += 1) {
35
+ bytes[index] = binary.charCodeAt(index);
36
+ }
37
+ return bytes.buffer;
38
+ }
39
+ /** The other direction. Chunked, because `String.fromCharCode` has an argument limit. */
40
+ export function base64Of(buffer) {
41
+ const bytes = new Uint8Array(buffer);
42
+ const chunk = 0x8000;
43
+ let binary = '';
44
+ for (let index = 0; index < bytes.length; index += chunk) {
45
+ binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
46
+ }
47
+ return btoa(binary);
48
+ }
49
+ /**
50
+ * Refuse a deployment whose bindings never arrived.
51
+ *
52
+ * The operator who gets this wrong wrote a `wrangler.jsonc` — spelled the
53
+ * binding `D1` instead of `DB`, or added the R2 bucket to production and not to
54
+ * a preview environment — and the compiler is not standing there. Without this,
55
+ * `undefined` is carried all the way to the first statement that touches it and
56
+ * surfaces as `Cannot read properties of undefined (reading 'prepare')` on one
57
+ * request, while routes that happen not to reach the missing half keep working.
58
+ *
59
+ * `worker-entry` already turns every construction refusal into a 500 whose body
60
+ * is the sentence, and it already falls back for `PROJECT` and `RETENTION_DAYS`
61
+ * rather than letting them fail late. The two bindings are the settings most
62
+ * likely to be wrong and were the only ones with no sentence.
63
+ *
64
+ * Shape-checked rather than merely present, because a binding pointing at the
65
+ * wrong kind of resource is the same mistake one line further along, and
66
+ * `R2Like` and `D1Like` share no method name for the two to be confused by.
67
+ * Split in two because `./history` takes a database and no bucket, and one
68
+ * function with an optional second argument cannot tell that door from a store
69
+ * whose bucket is missing.
70
+ */
71
+ export function requireD1(db) {
72
+ if (db === null || typeof db !== 'object' || typeof db.prepare !== 'function') {
73
+ throw new Error('`db` is not a D1 binding. A Worker receives it as `env.DB`, declared under `d1_databases` ' +
74
+ 'in `wrangler.jsonc` with the `binding` name matching what the entry reads; a name that ' +
75
+ 'does not match arrives as `undefined`, and the first request to touch a row is where you ' +
76
+ 'would otherwise hear about it');
77
+ }
78
+ }
79
+ /** The other half. See {@link requireD1}. */
80
+ export function requireR2(bucket) {
81
+ if (bucket === null || typeof bucket !== 'object' || typeof bucket.get !== 'function') {
82
+ throw new Error('`bucket` is not an R2 binding. A Worker receives it as `env.BUCKET`, declared under ' +
83
+ '`r2_buckets` in `wrangler.jsonc`; declaring it for production and not for a preview ' +
84
+ 'environment is the common shape of this, and the sidecar rows would be written without ' +
85
+ 'the images they describe');
86
+ }
87
+ }
88
+ //# sourceMappingURL=bindings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bindings.js","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAkFH;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,MAAc;IACpC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5C,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACtD,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC;AACtB,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,QAAQ,CAAC,MAAmB;IAC1C,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,MAAM,CAAC;IACrB,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,KAAK,EAAE,CAAC;QACzD,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,SAAS,CAAC,EAAW;IACnC,IAAI,EAAE,KAAK,IAAI,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,OAAQ,EAAa,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QAC1F,MAAM,IAAI,KAAK,CACb,4FAA4F;YAC1F,yFAAyF;YACzF,2FAA2F;YAC3F,+BAA+B,CAClC,CAAC;IACJ,CAAC;AACH,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,SAAS,CAAC,MAAe;IACvC,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAQ,MAAiB,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;QAClG,MAAM,IAAI,KAAK,CACb,sFAAsF;YACpF,sFAAsF;YACtF,yFAAyF;YACzF,0BAA0B,CAC7B,CAAC;IACJ,CAAC;AACH,CAAC","sourcesContent":["/**\n * The platform, named in full, in one file, by us.\n *\n * Every other module here takes a `D1Like` and an `R2Like` and never a\n * Cloudflare type. That is not portability theatre — it is the same rule\n * [ADR-0013](../../../docs/context/adr/0013-packages-are-named-for-their-requirements.md)\n * applies everywhere else in this repository: a package requires what it says it\n * requires. Depending on `@cloudflare/workers-types` would put a global\n * `DOM`-shaped ambient declaration into the build of a package the CLI links\n * against, and it would make the store untestable without a Workers runtime.\n *\n * What is declared below is exactly the surface used, and nothing else. It is\n * structurally satisfied by the real `D1Database` and `R2Bucket` — an operator's\n * Worker passes `env.DB` and `env.BUCKET` straight in and TypeScript accepts it,\n * because a wider interface is assignable to a narrower one.\n *\n * **The cost is stated rather than hidden.** These are hand-written shapes, so a\n * breaking change to D1 or R2 is a runtime failure here rather than a compile\n * error. The mitigation is that the surface is tiny: five methods on D1, three on\n * R2, all of them years old and all of them exercised by the tests through a\n * `node:sqlite`-backed double that runs the same SQL a real D1 would.\n */\n\n/** A value D1 accepts as a bound parameter. D1 has no `bigint` and no `Buffer`. */\nexport type D1Value = string | number | null | ArrayBuffer;\n\nexport interface D1PreparedLike {\n bind(...values: readonly D1Value[]): D1PreparedLike;\n first<Row>(): Promise<Row | null>;\n all<Row>(): Promise<{ readonly results: readonly Row[] }>;\n run(): Promise<unknown>;\n}\n\n/**\n * A SQL database that runs one statement or a batch of them.\n *\n * D1 is SQLite, so this is satisfied by the platform binding and by\n * `node:sqlite` through a thin wrapper, which is what lets the same store back\n * a Worker and a file on a machine somebody owns.\n */\nexport interface D1Like {\n prepare(sql: string): D1PreparedLike;\n\n /**\n * Several statements, committed together or not at all.\n *\n * The whole reason the history backend can satisfy its atomicity requirement\n * on a platform with no `BEGIN`. D1 wraps a batch in an implicit transaction\n * and rolls the whole thing back if any statement fails, which is what\n * [spec 0002](../../../docs/specs/0002-history-store.md) needs: rows without\n * their run leave a change with no denominator, and a run without its rows is\n * a quiet run that was not quiet.\n *\n * **Unverified against the platform.** See the known limits in\n * [ADR-0023](../../../docs/context/adr/0023-a-service-is-named-for-what-it-is.md) — the\n * double used in tests is genuinely transactional, and the claim that D1 is\n * has never been measured here.\n */\n batch(statements: readonly D1PreparedLike[]): Promise<unknown>;\n}\n\nexport interface R2ObjectLike {\n arrayBuffer(): Promise<ArrayBuffer>;\n}\n\n/**\n * An object store addressed by key, holding image bytes and nothing else.\n *\n * Four methods, because that is all the store uses: read, exists, write, remove.\n * A bucket is never listed and never scanned — every key this package reads is\n * one a row already named.\n */\nexport interface R2Like {\n /** `null` means the bucket answered and there is no such object. */\n get(key: string): Promise<R2ObjectLike | null>;\n /**\n * Existence without the bytes.\n *\n * Here for one reason: `describe` must agree with `find` about whether a\n * baseline exists. A sidecar row whose object is gone would be a description to\n * the cheap lookup and a corrupted pair to the full one, and the verdict would\n * then depend on which question the caller happened to ask. The directory store\n * spends a `stat` for exactly this; this spends a HEAD.\n */\n head(key: string): Promise<unknown | null>;\n put(key: string, value: ArrayBuffer): Promise<unknown>;\n delete(keys: string | readonly string[]): Promise<unknown>;\n}\n\n/**\n * The two bindings, together, because nothing here works with one of them.\n *\n * A deployment with a database and no bucket can record that a subject changed\n * and cannot show anyone what it looks like; a deployment with a bucket and no\n * database has images nobody can address. Taking them as a pair means the\n * missing one is a wiring error at construction rather than a `TypeError` on the\n * first request that happens to need it.\n */\nexport interface TribunalBindings {\n readonly db: D1Like;\n readonly bucket: R2Like;\n}\n\n/**\n * Base64 in, `ArrayBuffer` out — the one conversion this package does twice.\n *\n * A `Raster` carries its bytes as base64 precisely so it survives JSON hops, and\n * R2 stores bytes. `atob` is the conversion both Workers and Node 22 have; going\n * through `Buffer` would tie this file to Node and would be the only thing in the\n * package that could not run on the platform it is named for.\n */\nexport function bytesOf(base64: string): ArrayBuffer {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes.buffer;\n}\n\n/** The other direction. Chunked, because `String.fromCharCode` has an argument limit. */\nexport function base64Of(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n const chunk = 0x8000;\n let binary = '';\n for (let index = 0; index < bytes.length; index += chunk) {\n binary += String.fromCharCode(...bytes.subarray(index, index + chunk));\n }\n return btoa(binary);\n}\n\n/**\n * Refuse a deployment whose bindings never arrived.\n *\n * The operator who gets this wrong wrote a `wrangler.jsonc` — spelled the\n * binding `D1` instead of `DB`, or added the R2 bucket to production and not to\n * a preview environment — and the compiler is not standing there. Without this,\n * `undefined` is carried all the way to the first statement that touches it and\n * surfaces as `Cannot read properties of undefined (reading 'prepare')` on one\n * request, while routes that happen not to reach the missing half keep working.\n *\n * `worker-entry` already turns every construction refusal into a 500 whose body\n * is the sentence, and it already falls back for `PROJECT` and `RETENTION_DAYS`\n * rather than letting them fail late. The two bindings are the settings most\n * likely to be wrong and were the only ones with no sentence.\n *\n * Shape-checked rather than merely present, because a binding pointing at the\n * wrong kind of resource is the same mistake one line further along, and\n * `R2Like` and `D1Like` share no method name for the two to be confused by.\n * Split in two because `./history` takes a database and no bucket, and one\n * function with an optional second argument cannot tell that door from a store\n * whose bucket is missing.\n */\nexport function requireD1(db: unknown): void {\n if (db === null || typeof db !== 'object' || typeof (db as D1Like).prepare !== 'function') {\n throw new Error(\n '`db` is not a D1 binding. A Worker receives it as `env.DB`, declared under `d1_databases` ' +\n 'in `wrangler.jsonc` with the `binding` name matching what the entry reads; a name that ' +\n 'does not match arrives as `undefined`, and the first request to touch a row is where you ' +\n 'would otherwise hear about it',\n );\n }\n}\n\n/** The other half. See {@link requireD1}. */\nexport function requireR2(bucket: unknown): void {\n if (bucket === null || typeof bucket !== 'object' || typeof (bucket as R2Like).get !== 'function') {\n throw new Error(\n '`bucket` is not an R2 binding. A Worker receives it as `env.BUCKET`, declared under ' +\n '`r2_buckets` in `wrangler.jsonc`; declaring it for production and not for a preview ' +\n 'environment is the common shape of this, and the sidecar rows would be written without ' +\n 'the images they describe',\n );\n }\n}\n"]}
@@ -0,0 +1,132 @@
1
+ import { type RegionRecord } from '@variance-authority/report';
2
+ import type { D1Like } from './bindings.js';
3
+ /**
4
+ * Why a baseline in this database is what it is.
5
+ *
6
+ * The other half of the same subsystem the git-LFS backend answers with a commit
7
+ * message. There, the baseline is a commit and the explanation goes in the
8
+ * message; here, the baseline is a row and the explanation goes in a row beside
9
+ * it. Both are read back by asking the store that holds the bytes, which is the
10
+ * only arrangement in which the explanation cannot drift away from the thing it
11
+ * explains.
12
+ *
13
+ * ## Why it copies rather than joins
14
+ *
15
+ * Everything in this table is already in `build_subjects` and `builds` at the
16
+ * moment it is written, and a view over those two would be shorter. It would also
17
+ * be empty after `sweep`: builds expire — that is what a review surface's storage
18
+ * budget is — and the explanation of a baseline has to outlive the build that
19
+ * proposed it by exactly as long as the baseline lasts, which is forever. So the
20
+ * regions, the commit, the intent and the reviewer are **frozen** into the row at
21
+ * the moment of approval, the same way a commit message is frozen into a commit.
22
+ *
23
+ * A row is written only for an **approval**. A rejection is a decision and it is
24
+ * recorded in `decisions`, but no baseline changed, and a changelog that carried
25
+ * rejections would answer *why does this baseline look like this* with entries
26
+ * about baselines that are not there.
27
+ *
28
+ * ## Clustered on the way out, not on the way in
29
+ *
30
+ * One row per approved subject, and the shapes are grouped when somebody reads.
31
+ * Approval here is per subject — a reviewer clicks through a docket rather than
32
+ * running one command over a report — so there is no moment at which a "batch"
33
+ * exists to cluster. Grouping at read time also means a shape approved across
34
+ * three sessions reads as one change, which is what it was.
35
+ */
36
+ /** One approved subject, exactly as the build described it. */
37
+ export interface ChangelogRow {
38
+ readonly build: string;
39
+ readonly subject: string;
40
+ readonly commit: string;
41
+ readonly intent?: string;
42
+ readonly by: string;
43
+ readonly note?: string;
44
+ readonly at: string;
45
+ /**
46
+ * The regions the build reported, frozen.
47
+ *
48
+ * The only measurement kept, and kept because the fingerprints inside it are
49
+ * what a shape *is* — the grouping is recomputed from these on every read. No
50
+ * aggregate sits beside them: a changed-pixel total measures displacement
51
+ * rather than magnitude and is bound to the machine that rendered it, and a
52
+ * column of them in a permanent table is an invitation to compare two numbers
53
+ * that were never comparable.
54
+ */
55
+ readonly regions: readonly RegionRecord[];
56
+ }
57
+ /** One change, as it was approved — the same unit the commit-message half uses. */
58
+ export interface ChangelogChange {
59
+ readonly fingerprint: string;
60
+ readonly component?: string;
61
+ readonly file?: string;
62
+ /**
63
+ * Approved subjects this shape landed in, most recently approved first.
64
+ *
65
+ * Approval order rather than a sort, and the same order as `by` beside it, so
66
+ * the two lists can be read against each other: the reviewer at the top is the
67
+ * one who approved the subject at the top.
68
+ */
69
+ readonly subjects: readonly string[];
70
+ /** The builds the approvals came from, newest first. */
71
+ readonly builds: readonly string[];
72
+ /** Everyone who approved part of this shape, most recent first. */
73
+ readonly by: readonly string[];
74
+ /** The most recent approval in the group. */
75
+ readonly at: string;
76
+ readonly intent?: string;
77
+ readonly note?: string;
78
+ }
79
+ export interface TribunalChangelog {
80
+ readonly changes: readonly ChangelogChange[];
81
+ /**
82
+ * Approved subjects no shape could group.
83
+ *
84
+ * A run that attributed nothing — the ephemeral and raster-only paths — still
85
+ * approved something, and counting it is how the total stays a total. Dropping
86
+ * these would make a changelog that covers eleven of forty approvals look like
87
+ * a changelog of eleven approvals.
88
+ */
89
+ readonly ungrouped: readonly ChangelogRow[];
90
+ }
91
+ export interface TribunalChangelogQuery {
92
+ /** Substring, case-insensitive, against the component a shape was attributed to. */
93
+ readonly component?: string;
94
+ /** Exact, because a subject id is exact. */
95
+ readonly subject?: string;
96
+ /** Rows to read, newest first. Defaults to 500. */
97
+ readonly limit?: number;
98
+ /** Only approvals at or after this instant. ISO 8601. */
99
+ readonly since?: string;
100
+ }
101
+ /**
102
+ * Freeze the explanation of one approval.
103
+ *
104
+ * Called from `decide` after the promotion and before the decision row, in the
105
+ * same order and for the same reason: the promotion is the thing with a
106
+ * consequence, and a changelog entry for a baseline that was never written would
107
+ * be an explanation of nothing.
108
+ *
109
+ * Never invents a build's `commit` or `intent` — both come from the build row the
110
+ * approval is about, and a build that cannot be read is a refusal in `promote`
111
+ * long before this runs.
112
+ */
113
+ export declare function recordApproval(db: D1Like, project: string, input: {
114
+ readonly build: string;
115
+ readonly subject: string;
116
+ readonly by: string;
117
+ readonly note?: string;
118
+ readonly at: string;
119
+ readonly commit: string;
120
+ readonly intent?: string;
121
+ readonly regions: string;
122
+ }): Promise<void>;
123
+ /**
124
+ * Every baseline this project has promoted, grouped by what changed.
125
+ *
126
+ * The filters narrow what is *read*, never what is explained: `--subject` selects
127
+ * rows and then the shapes those rows carry are grouped whole, so a shape
128
+ * approved in forty subjects still reports forty when you ask about one of them.
129
+ * Answering with a group of one would say the change was smaller than it was.
130
+ */
131
+ export declare function readChangelog(db: D1Like, project: string, query?: TribunalChangelogQuery): Promise<TribunalChangelog>;
132
+ //# sourceMappingURL=changelog.d.ts.map