@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,100 @@
1
+ # @variance-authority/tribunal
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c54a8d1: Serve every history path, and let a reviewer ask what the record already knows.
8
+
9
+ Three of the eight protocol paths were unserved and two fields were dropped on
10
+ the way in, and none of it presented as an error. Without `/v1/current` a run
11
+ compares against nothing, so every component reads as new and drift is never
12
+ detected. Without `/v1/approvals` every observation stays unapproved, so churn
13
+ answers *this component has never changed* about a component that changed forty
14
+ times. Without `instabilities` and `run.swept` a flake has no denominator. All
15
+ three failures sound like a stable suite.
16
+
17
+ The derived reads — churn, reach, flakiness, the value journey, the last change —
18
+ now answer the review capability as well as the ingest one. They write nothing,
19
+ and the browser drawing a review page holds the review token; refusing them there
20
+ meant a surface that can approve a change it cannot put in context. The writes
21
+ and `/v1/current` stay the ingest token's.
22
+
23
+ On the page: a **Changelog** tab grouping every approval by the shape that was
24
+ approved, counting the approvals nothing could attribute rather than dropping
25
+ them, and a per-subject **record** panel — how often this subject read
26
+ differently, whether it has since, how often its cause caused an approved change,
27
+ and how far that component reaches. Fetched when the reviewer asks, because a
28
+ build with three hundred changed subjects would otherwise make nine hundred
29
+ requests to draw a page on which one is read. A flake rate stays absent until
30
+ something swept: `0%` is the confident answer to a question nobody asked.
31
+ - 89bd179: Export `MIGRATIONS` and `INITIAL_VERSION` from the package entrypoint.
32
+
33
+ The README sends an operator whose database is already deployed to `MIGRATIONS`
34
+ — "what an existing database needs is the part it is missing" — and the constant
35
+ lived in a private module. The entrypoint offered `SCHEMA`, which fails on the
36
+ first `CREATE TABLE` against that database by design, and nothing else. There
37
+ was no way to reach the steps from outside the package.
38
+
39
+ `INITIAL_VERSION` goes with it, because `MIGRATIONS` is indexed against it: step
40
+ `i` lands on `INITIAL_VERSION + i + 1`, so a database reporting `schema_version`
41
+ `n` needs every step from `n - INITIAL_VERSION` on.
42
+ - f2564ed: Name the sweep count for what happened to it
43
+
44
+ `SweepReport.decisions` became `decisionsKept`. Every other number in that
45
+ report is a removal, and the README sentence beside it says the store "reports
46
+ counts for everything it removed" — so an operator reading `decisions: 4`
47
+ concludes four approvals were deleted, which is the one thing the `decisions`
48
+ table's permanence trigger exists to make impossible. The number was always the
49
+ opposite: approvals that outlived the builds this call removed.
50
+ - 896d64a: Run the review service on a machine you own — a SQLite file, a directory, a port.
51
+
52
+ The package shipped one deployment and named none: every module above the
53
+ bindings takes a `D1Like` and an `R2Like`, so what stood between it and a laptop
54
+ was two adapters and a shell. `@variance-authority/tribunal/node` supplies them.
55
+ `openDatabase` opens, creates and migrates a SQLite file through `node:sqlite`
56
+ and reports the version it settled on; `createDirectoryBucket` puts objects in a
57
+ directory through `node:fs`, each written to a staging file and renamed so a
58
+ reader never sees half of one, and refusing a key a case-folding volume would
59
+ land on another key's file. `serveTribunal` binds a `node:http` server over the
60
+ same `createTribunalRoutes` the Next.js adapter uses, so the token-attaching rule
61
+ has one implementation rather than two.
62
+
63
+ `variance-authority-tribunal` is that service as an executable, configured by the
64
+ environment. It refuses an unnamed project, refuses a non-loopback bind unless
65
+ `VARIANCE_TRIBUNAL_TRUST_NETWORK` says so, and on a network bind serves no review
66
+ surface at all — there would be nothing between an approve button and the
67
+ internet. On loopback it serves the surface and treats an untokened caller as the
68
+ reviewer, because anything that can open the port is already the person who
69
+ started the process. No token is written into the page or into the startup line.
70
+
71
+ The SQLite adapter `testing.ts` had privately is now that shipped adapter, so
72
+ what the suite exercises is what an operator runs.
73
+
74
+ ### Patch Changes
75
+
76
+ - 44a174f: Name the binding that never arrived
77
+
78
+ `createBucketStore` and `createD1Backend` now refuse a `db` or `bucket` that is
79
+ not the binding it claims to be. A `wrangler.jsonc` declaring the database as
80
+ `D1` while the entry reads `env.DB` used to reach the first statement that
81
+ touched a row and surface as `Cannot read properties of undefined (reading
82
+ 'prepare')` — a platform-shaped error for a configuration line — and a bucket
83
+ declared for production and not for a preview environment would have written
84
+ sidecar rows without the images they describe.
85
+ - 08ab85c: Refuse a deployment that never said which project it is.
86
+
87
+ `project` is required, scopes every row and every object key, and had no runtime
88
+ check. `undefined` reached D1 as a bind parameter and came back as *the baseline
89
+ store could not reach its database or its bucket* — the platform blamed for a
90
+ line in a wrangler file — and a blank string quietly became a namespace nobody
91
+ named. `createBucketStore` now refuses both with a sentence, which `createTribunal`
92
+ and `createReviewStore` inherit, the way the two token rules already answer.
93
+ - Updated dependencies [1d402d1]
94
+ - Updated dependencies [e8fee66]
95
+ - Updated dependencies [5c34e6d]
96
+ - @variance-authority/report@0.1.0
97
+ - @variance-authority/core@0.1.0
98
+ - @variance-authority/history@0.1.0
99
+ - @variance-authority/raster@0.1.0
100
+ - @variance-authority/server@0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Machine Garden
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,506 @@
1
+ <p align="center"><img src="./mark.svg" alt="Variance Authority mark" width="72"></p>
2
+
3
+ # @variance-authority/tribunal
4
+
5
+ > A self-hosted review service for Variance Authority baselines, history and per-subject decisions.
6
+
7
+ Use this package when you are deploying a self-hosted review service for
8
+ baselines, history, and per-subject decisions. It supplies the Worker handler,
9
+ storage adapters, review API, and optional React UI; it does not provide a hosted
10
+ endpoint or an integration that posts runs for you.
11
+
12
+ The deployment owns the database and the object store, the runtime has to serve
13
+ `fetch`, and it takes two different bearer tokens of at least 16 characters.
14
+
15
+ **Two deployments ship, and they run the same router.** On Cloudflare the
16
+ database is D1, the object store is R2, and the runtime is a Worker. On a machine
17
+ you own — a laptop, an EC2 instance, a container, the package does not ask — the
18
+ database is a SQLite file through `node:sqlite` and the object store is a
19
+ directory through `node:fs`. Every module above the bindings takes a `D1Like` and
20
+ an `R2Like` and names no runtime, so the routes, the refusals and the status
21
+ codes are the same code in both. The name says what it is rather than where it
22
+ runs.
23
+
24
+ Anything else that satisfies those two interfaces is an adapter away, but the
25
+ package makes no portability promise beyond the two it ships; the operator owns
26
+ their deployment's platform limits.
27
+
28
+ Producing runs and reports is the job of `@variance-authority/cli`. This package
29
+ stores the evidence they send and gives a reviewer a place to inspect and settle
30
+ it; it does not render subjects or decide what a run should contain.
31
+
32
+ ```bash
33
+ npm install @variance-authority/tribunal
34
+ ```
35
+ ## The service surfaces
36
+
37
+ | surface | contract | defined by |
38
+ |---|---|---|
39
+ | baselines | `RasterStore` behind `/baseline/*` and `/cache/*` | `raster`, `remote` |
40
+ | history | `HistoryBackend` behind `/v1/*` — every path the protocol defines | `history`, `server` |
41
+ | review | builds, subjects, decisions | here |
42
+
43
+ `variance run` reaches this deployment with **no change to the CLI** —
44
+ `baselines.kind: "remote"` and a URL — and a client built from
45
+ `@variance-authority/history/client` reaches it with no change to the client.
46
+
47
+ A **subject** is one rendered unit under test — a story, route, or fixture —
48
+ identified by a string id (for example `story:card`). `@variance-authority/cli`
49
+ decides each subject's verdict (`unchanged`, `changed`, `new`, among others)
50
+ upstream; this service stores the evidence and the decision made about it.
51
+
52
+ ## Entrypoints
53
+
54
+ | entrypoint | requires | holds |
55
+ |---|---|---|
56
+ | `@variance-authority/tribunal` | nothing | the binding types, `SCHEMA`, `applySchema`, `MIGRATIONS` |
57
+ | `@variance-authority/tribunal/store` | D1 and R2 | `createBucketStore` — baselines |
58
+ | `@variance-authority/tribunal/history` | D1 | `createD1Backend` — the drift record |
59
+ | `@variance-authority/tribunal/review` | D1 and R2 | `createReviewStore` — builds, decisions, retention |
60
+ | `@variance-authority/tribunal/worker` | D1, R2, two tokens | `createTribunal` — one `fetch` handler |
61
+ | `@variance-authority/tribunal/worker-entry` | the bindings, as an `env` | the deployable module: `export default { fetch }`, and `wrangler.jsonc` beside it |
62
+ | `@variance-authority/tribunal/node` | Node 22, a writable file and directory | `openDatabase`, `createDirectoryBucket`, `serveTribunal` — the same service over `node:sqlite`, `node:fs` and `node:http`, plus the `variance-authority-tribunal` executable |
63
+ | `@variance-authority/tribunal/ui` | React | the review surface, its JSON client, its stylesheet |
64
+ | `@variance-authority/tribunal/next` | an App Router app | `createTribunalRoutes` — route handlers for a Next.js App Router |
65
+ | `@variance-authority/tribunal/testing` | Node 22 | D1 over `node:sqlite`, an in-memory bucket |
66
+
67
+ **The table is for reading, not for slimming an install.** This is one service,
68
+ deployed once, so React is an ordinary production dependency even though only
69
+ `/ui` and `/next` touch it. The rows above say which entrypoint needs what,
70
+ which is worth knowing when you read the code or split the deployment
71
+ across two Workers, not because the package is trying to keep an install small.
72
+
73
+ ## Wiring it up
74
+
75
+ ```ts
76
+ // worker.ts — yours, not ours
77
+ import type { D1Like, R2Like } from '@variance-authority/tribunal';
78
+ import { createTribunal } from '@variance-authority/tribunal/worker';
79
+
80
+ interface Env {
81
+ readonly DB: D1Like;
82
+ readonly BUCKET: R2Like;
83
+ readonly VARIANCE_INGEST_TOKEN: string;
84
+ readonly VARIANCE_REVIEW_TOKEN: string;
85
+ }
86
+
87
+ export default {
88
+ fetch(request: Request, env: Env): Promise<Response> {
89
+ return createTribunal({
90
+ db: env.DB,
91
+ bucket: env.BUCKET,
92
+ project: 'todomvc',
93
+ ingestToken: env.VARIANCE_INGEST_TOKEN,
94
+ reviewToken: env.VARIANCE_REVIEW_TOKEN,
95
+ }).fetch(request);
96
+ },
97
+ };
98
+ ```
99
+
100
+ `createTribunal` takes:
101
+
102
+ | option | default | what it decides |
103
+ |---|---|---|
104
+ | `db` | required | the D1 binding |
105
+ | `bucket` | required | the R2 binding |
106
+ | `project` | required | scopes every row and every object key, so one deployment serves several repositories without their `story:card` colliding. There is no default: an invented one puts two projects' baselines in one namespace and the first symptom is a mass `changed` |
107
+ | `ingestToken` | required | written into CI. Writes builds, baselines and history. 16 characters or more |
108
+ | `reviewToken` | required | held by people. Reads the review surface and decides. 16 characters or more, and not the same string as `ingestToken` |
109
+ | `retentionDays` | `30` | days of builds `POST /review/sweep` keeps. Applied on request rather than on a timer, because a Worker has no timer and this package will not invent a cron the operator did not ask for — wire it to a scheduled trigger, call it from a CI job, or never |
110
+ | `now` | the wall clock | supplies every recorded `at`; override it when the deployment has its own clock source |
111
+
112
+ `env.DB` and `env.BUCKET` are Cloudflare's own `D1Database` and `R2Bucket`,
113
+ accepted as-is via structural typing: this package declares only the subset it
114
+ uses, and does **not** depend on `@cloudflare/workers-types` — which is why the
115
+ store is testable in a plain `vitest` process.
116
+
117
+ Apply the schema yourself, once — `applySchema(db)`, or the statements in
118
+ `SCHEMA` through whatever you use for migrations. Nothing here migrates on a
119
+ request, since D1 has no advisory lock to serialize a migration running under
120
+ concurrent load.
121
+
122
+ An **already-deployed** database is not upgraded by that call: `SCHEMA` is the
123
+ whole shape, and applying it a second time fails on the first `CREATE TABLE`.
124
+ Apply `MIGRATIONS` instead — one entry per version after the package's initial
125
+ shape, each writing the version it lands on. Step `i` lands on
126
+ `INITIAL_VERSION + i + 1`, so a database reporting `schema_version` `n` needs
127
+ every step from `n - INITIAL_VERSION` on.
128
+
129
+ ### Or deploy the one that ships
130
+
131
+ The module above is the shape to copy when you want your own. If you do not,
132
+ `worker-entry` **is** that module, and `wrangler.jsonc` beside it declares the
133
+ Worker, the D1 database and the R2 bucket:
134
+
135
+ ```bash
136
+ wrangler d1 create variance-tribunal
137
+ ```
138
+
139
+ Put the id it prints into `wrangler.jsonc`, then:
140
+
141
+ ```bash
142
+ wrangler r2 bucket create variance-tribunal
143
+ ```
144
+
145
+ ```bash
146
+ wrangler d1 migrations apply variance-tribunal --remote
147
+ ```
148
+
149
+ ```bash
150
+ wrangler secret put INGEST_TOKEN
151
+ ```
152
+
153
+ ```bash
154
+ wrangler secret put REVIEW_TOKEN
155
+ ```
156
+
157
+ ```bash
158
+ wrangler deploy
159
+ ```
160
+
161
+ `worker-entry` is the only file in the package that reads an environment, and it
162
+ reads four names plus two optional ones:
163
+
164
+ | name | kind | what it decides |
165
+ |---|---|---|
166
+ | `DB` | binding | D1. The schema is in `migrations/`. A name that does not match what `d1_databases` declares is refused by name |
167
+ | `BUCKET` | binding | R2. Baseline and candidate bytes; never a row. Declared for production and not for a preview environment is refused by name |
168
+ | `INGEST_TOKEN` | secret | written into CI. Writes builds, baselines and history. 16 characters or more |
169
+ | `REVIEW_TOKEN` | secret | held by people. Reads the review surface and decides. 16 characters or more |
170
+ | `PROJECT` | var, default `default` | scopes every row and object |
171
+ | `RETENTION_DAYS` | var, default `30` | days of builds `POST /review/sweep` keeps. A value that is not a positive finite number falls back rather than sweeping everything |
172
+
173
+ A bad environment answers **500 with a sentence**, not a deployment-wide platform
174
+ error: construction happens inside `fetch`, so *your token is too short* and *your
175
+ two tokens are the same* reach the operator as the response body.
176
+
177
+ `PROJECT` is a deployment setting, so one deployment serves one project; a
178
+ second tenant needs a second deployment.
179
+
180
+ The migrations in `migrations/` are **generated** from `SCHEMA` by
181
+ `tools/tribunal-migrations.mjs` — change `schema.ts` and rebuild rather than
182
+ editing a generated `.sql` file. `wrangler.jsonc` carries the project name and
183
+ no credentials; keep both tokens in Wrangler secrets.
184
+
185
+ ### Or run it on a machine you own
186
+
187
+ No account, no `wrangler`, no platform. `variance-authority-tribunal` is the same
188
+ service over a SQLite file and a directory, and it is configured entirely by the
189
+ environment so that nothing about a deployment lives in a shell history:
190
+
191
+ ```bash
192
+ VARIANCE_TRIBUNAL_PROJECT=todomvc VARIANCE_TRIBUNAL_INGEST_TOKEN=$INGEST VARIANCE_TRIBUNAL_REVIEW_TOKEN=$REVIEW npx variance-authority-tribunal
193
+ ```
194
+
195
+ | variable | default | what it decides |
196
+ |---|---|---|
197
+ | `VARIANCE_TRIBUNAL_PROJECT` | required | scopes every row and object key. No default, for the reason `project` has none above |
198
+ | `VARIANCE_TRIBUNAL_INGEST_TOKEN` | required | written into CI. Writes builds, baselines and history. 16 characters or more |
199
+ | `VARIANCE_TRIBUNAL_REVIEW_TOKEN` | required | held by people. Reads the review surface and decides. 16 characters or more, and not the ingest token |
200
+ | `VARIANCE_TRIBUNAL_PORT` | `7789` | a whole number, or the process refuses to start |
201
+ | `VARIANCE_TRIBUNAL_HOST` | `127.0.0.1` | the bind address. Anything reachable from off the machine also needs `VARIANCE_TRIBUNAL_TRUST_NETWORK` |
202
+ | `VARIANCE_TRIBUNAL_DB` | `variance-tribunal.db` | the SQLite file. Created and migrated on start; the startup line prints its absolute path and its schema version |
203
+ | `VARIANCE_TRIBUNAL_STORAGE` | `variance-tribunal-objects` | the directory holding baseline and candidate bytes |
204
+ | `VARIANCE_TRIBUNAL_RETENTION_DAYS` | the package default | days of builds `POST /review/sweep` keeps |
205
+ | `VARIANCE_TRIBUNAL_REVIEWER` | the OS user | the name written on decisions made through the served UI |
206
+ | `VARIANCE_TRIBUNAL_TRUST_NETWORK` | unset | confirms a non-loopback bind, and see what it costs below |
207
+
208
+ **On loopback, a browser is a reviewer.** The bare URL serves the review surface,
209
+ and a caller with no token is treated as holding the review token — because
210
+ anything that can open `127.0.0.1:7789` is already running as the person who
211
+ started it, and asking them to paste their own secret back to themselves buys
212
+ nothing. **On a network bind the review surface is not served at all**, and no
213
+ call is authorized without a token: there would be nothing between an approve
214
+ button and the internet. Put the Next.js adapter behind your own sign-in for
215
+ that, or leave the process on loopback behind a proxy that authenticates.
216
+
217
+ **The tokens are never printed and never rendered.** The startup line names the
218
+ variables, not their values, and the served page carries the endpoint and the
219
+ reviewer name and nothing else — the browser calls the service, which holds the
220
+ token, exactly as it does behind Next.js.
221
+
222
+ The schema is applied and migrated on start. That is the one thing the Node shell
223
+ does that the Worker will not: a file has an exclusive lock and a single writer,
224
+ so there is no concurrent-migration race to lose.
225
+
226
+ Composing it yourself is the same three pieces without the executable, which is
227
+ what you want when the process is also serving something else:
228
+
229
+ ```ts
230
+ import { createDirectoryBucket, openDatabase, serveTribunal } from '@variance-authority/tribunal/node';
231
+ import { createTribunal } from '@variance-authority/tribunal/worker';
232
+
233
+ const ingest = process.env.INGEST_TOKEN ?? '';
234
+ const review = process.env.REVIEW_TOKEN ?? '';
235
+
236
+ const service = await serveTribunal({
237
+ tribunal: createTribunal({
238
+ db: await openDatabase('variance-tribunal.db'),
239
+ bucket: createDirectoryBucket('variance-tribunal-objects'),
240
+ project: 'todomvc',
241
+ ingestToken: ingest,
242
+ reviewToken: review,
243
+ }),
244
+ host: '127.0.0.1',
245
+ port: 7789,
246
+ authorize: (request) =>
247
+ request.headers.get('authorization') === `Bearer ${ingest}` ? 'ingest' : 'review',
248
+ tokens: { ingest, review },
249
+ ui: true,
250
+ reviewer: 'marina',
251
+ });
252
+
253
+ service.url; // http://127.0.0.1:7789
254
+ await service.close();
255
+ ```
256
+
257
+ `serveTribunal` takes the `tribunal` to serve, an `authorize` and `tokens` that
258
+ mean exactly what they mean for `createTribunalRoutes` — it is the same function
259
+ underneath — plus `host`, `port`, and two that only a served page needs: `ui`,
260
+ which decides whether the review surface and its bundle are served at all, and
261
+ `reviewer`, the name written on decisions made through it. `openDatabase` opens,
262
+ creates or migrates the file and reports the `version` it settled on;
263
+ `createDirectoryBucket` writes each object through a staging file and renames it,
264
+ so a reader never sees half of one.
265
+
266
+ ### The review surface
267
+
268
+ ```tsx
269
+ // app/variance/page.tsx
270
+ import { ReviewApp, REVIEW_STYLES, createReviewClient } from '@variance-authority/tribunal/ui';
271
+
272
+ declare function whoIsThis(): Promise<string>;
273
+
274
+ const client = createReviewClient({ endpoint: '/variance' });
275
+
276
+ export default async function VariancePage(): Promise<React.ReactElement> {
277
+ return (
278
+ <>
279
+ <style>{REVIEW_STYLES}</style>
280
+ <ReviewApp client={client} reviewer={await whoIsThis()} />
281
+ </>
282
+ );
283
+ }
284
+ ```
285
+
286
+ ```ts
287
+ // app/variance/[[...path]]/route.ts
288
+ import type { D1Like, R2Like } from '@variance-authority/tribunal';
289
+ import { createTribunalRoutes } from '@variance-authority/tribunal/next';
290
+ import { createTribunal } from '@variance-authority/tribunal/worker';
291
+
292
+ declare const env: {
293
+ readonly DB: D1Like;
294
+ readonly BUCKET: R2Like;
295
+ readonly VARIANCE_INGEST_TOKEN: string;
296
+ readonly VARIANCE_REVIEW_TOKEN: string;
297
+ };
298
+ declare function isSignedIn(request: Request): Promise<boolean>;
299
+
300
+ const worker = createTribunal({
301
+ db: env.DB,
302
+ bucket: env.BUCKET,
303
+ project: 'todomvc',
304
+ ingestToken: env.VARIANCE_INGEST_TOKEN,
305
+ reviewToken: env.VARIANCE_REVIEW_TOKEN,
306
+ });
307
+
308
+ export const { GET, POST, HEAD } = createTribunalRoutes(worker, {
309
+ basePath: '/variance',
310
+ authorize: async (request: Request) => ((await isSignedIn(request)) ? 'review' : null),
311
+ tokens: { ingest: env.VARIANCE_INGEST_TOKEN, review: env.VARIANCE_REVIEW_TOKEN },
312
+ });
313
+ ```
314
+
315
+ `createReviewClient` takes `endpoint` — where the Worker is mounted — and an
316
+ optional `token` for a caller holding the review token directly. Omit `token`
317
+ behind the Next.js adapter above: the server route holds it and the browser
318
+ never sees it.
319
+
320
+ `createTribunalRoutes` takes `basePath`, `authorize` and `tokens`. `basePath` is
321
+ stripped before the request reaches the Worker. `tokens` is passed explicitly
322
+ because the `Tribunal` object only exposes `fetch`, not the tokens it was
323
+ created with.
324
+
325
+ `authorize` has **no default**. It returns `'ingest'`, `'review'`, or `null`; a
326
+ refused caller gets a 401 before the Worker sees the request, and a caller
327
+ returned `'review'` has the review token attached on their behalf.
328
+
329
+ ### `GET /review/changelog`
330
+
331
+ A build says what changed today; this endpoint says what was *approved*,
332
+ grouped by shape rather than by which screenshot changed — the same grouping
333
+ the docket (the reviewer's ranked list of causes, described below) uses, so a
334
+ token edit across forty stories is one entry, not forty.
335
+
336
+ ```ts
337
+ import { createReviewStore } from '@variance-authority/tribunal/review';
338
+ import { createMemoryR2, createSqliteD1 } from '@variance-authority/tribunal/testing';
339
+
340
+ const review = createReviewStore({
341
+ db: await createSqliteD1(),
342
+ bucket: createMemoryR2(),
343
+ project: 'todomvc',
344
+ });
345
+
346
+ const { changes, ungrouped } = await review.changelog({ component: 'Card', limit: 50 });
347
+
348
+ changes[0]?.subjects; // approved subjects this shape landed in, newest first
349
+ changes[0]?.by; // everyone who approved part of it
350
+ changes[0]?.builds; // where the approvals came from
351
+ ```
352
+
353
+ `changelog` takes `component` (substring, case-insensitive), `subject` (exact),
354
+ `since` (ISO 8601) and `limit` (default 500). The route takes the same four as
355
+ query parameters.
356
+
357
+ **One row is written per approval, and its columns are copies rather than a
358
+ join.** The regions, the commit, the intent and the reviewer are frozen at the
359
+ moment of approval rather than read live from `builds` and `build_subjects`,
360
+ because a `sweep` (see Retention below) removes builds, and a baseline's
361
+ explanation has to outlive them.
362
+
363
+ **Nothing is written for a rejection.** It is recorded in `decisions`, but no
364
+ baseline changed.
365
+
366
+ **Shapes are grouped when somebody reads**, not when a row is written — approval
367
+ here is per subject, so there is no batch at write time to cluster, and a shape
368
+ approved across several sessions still reads as one change. Approved subjects
369
+ that no shape could group are returned as `ungrouped` rather than dropped.
370
+
371
+ ## Review surface
372
+
373
+ When `ReviewApp` (`@variance-authority/tribunal/ui`) is open on a build, a
374
+ reviewer sees, in this order:
375
+
376
+ 1. **The docket** — one entry per component the semantic tier (the analysis step
377
+ that attributes a changed region to a component, rather than just measuring
378
+ pixels) named as a *cause*, largest first, with the file each is declared in.
379
+ Collateral is one number for the build rather than a per-region list.
380
+ 2. **The regions, drawn on the render**, cause and collateral styled apart, each
381
+ labelled with the component that owns it.
382
+ 3. **The comparison** — swipe, onion, side-by-side, difference mask — last, and
383
+ only the modes this build actually kept images for.
384
+ 4. **The record**, on request per subject: how often this subject has failed to
385
+ read the same way twice, how often its cause has caused an approved change,
386
+ and how many subjects that component reaches. Fetched when a reviewer clicks
387
+ *Has this changed before?* rather than with the build — a build with three
388
+ hundred changed subjects would otherwise make nine hundred history requests to
389
+ draw a page on which one is read.
390
+
391
+ The docket ranks by cause pixels rather than total area, so a large container
392
+ that only reflowed does not outrank the smaller edit that caused it; `cause` is
393
+ a field on a region rather than something inferred from a component's size.
394
+
395
+ Step 4 is the question a before-and-after cannot answer. The same 2px shift is a
396
+ bug in a component nobody has touched since March and a Tuesday in one that moves
397
+ in nineteen runs out of twenty. Two numbers are drawn as missing rather than as
398
+ zero, deliberately: a flake rate is **absent** until a run has read every subject
399
+ twice (`RunRecord.swept`), and a coverage that was never stated is unknown rather
400
+ than clean.
401
+
402
+ The `Changelog` tab is the same evidence at project scale — every approval,
403
+ grouped by the shape that was approved, with the approvals nothing could
404
+ attribute listed rather than dropped.
405
+
406
+ ## Review invariants
407
+
408
+ **Approval promotes an image; it never records one.** Deciding *approved* makes
409
+ that build's uploaded candidate the baseline, through the same `RasterStore` the
410
+ next run reads. A subject whose candidate was never uploaded **cannot be
411
+ approved**.
412
+
413
+ **A store failure is never a verdict.** Every D1 and R2 failure raises
414
+ `RasterStoreError` rather than returning a value; `null` is reserved for *the
415
+ store looked and there is no baseline*.
416
+
417
+ **A row without its object is damage, not absence.** The sidecar (metadata)
418
+ lives in D1 and the image in R2. `describe` spends an R2 `head` call to confirm
419
+ the object still exists rather than answering from the D1 row alone.
420
+
421
+ **A coverage list that was never stated is not an empty one.** `undefined` and
422
+ `[]` are stored, returned, and drawn as different values: absent means nothing
423
+ looked, `[]` means inspected and clean. The same distinction holds for findings.
424
+
425
+ **The review token reads the record and never writes it.** `/v1/churn`,
426
+ `/v1/reach`, `/v1/flakiness`, `/v1/value-journey` and `/v1/last-changed` answer
427
+ either capability, because they derive from rows already recorded and the browser
428
+ drawing a review page holds the review token. `/v1/observations`, `/v1/approvals`
429
+ and `/v1/current` are the ingest token's: the first two write, and the third is a
430
+ run asking what to write against.
431
+
432
+ **Two tokens, and they may not be equal.** The ingest token lives in CI
433
+ configuration and writes builds, baselines and history; the review token
434
+ belongs to people and decides. Construction refuses a token under 16 characters
435
+ and refuses two identical tokens.
436
+
437
+ **Authentication happens before routing.** A caller holding neither token gets
438
+ one identical response for a wrong token, a missing token, and a path that does
439
+ not exist; which of the two tokens a route wants is only revealed to a caller
440
+ who already holds a valid one.
441
+
442
+ **The Worker makes no outbound request.** Not a status check, not a PR comment,
443
+ not a webhook, not telemetry — the pipeline reports to the server, and the
444
+ server reports to nobody.
445
+
446
+ **The record is append-only, and the database enforces it.** Runs,
447
+ observations, token values and decisions all carry `UPDATE` and `DELETE`
448
+ triggers, so a direct `wrangler d1 execute` against the database is refused
449
+ too.
450
+
451
+ ## Retention
452
+
453
+ `POST /review/sweep?days=N` removes builds older than `N` days: their subject
454
+ rows, their coverage rows, and every image they kept. It **reports counts** for
455
+ everything it removed — `builds`, `subjects`, `objects`.
456
+
457
+ What it does not remove: **promoted baselines** (what the next run compares
458
+ against), **decisions**, and the **changelog**. The fourth count,
459
+ `decisionsKept`, is how many approvals outlived the builds this call removed,
460
+ not a fourth removal.
461
+
462
+ It runs on request only, never on a timer — wire it to a scheduled trigger or
463
+ call it from CI.
464
+
465
+ ## Testing your own wiring
466
+
467
+ ```ts
468
+ import { createReviewStore } from '@variance-authority/tribunal/review';
469
+ import { createMemoryR2, createSqliteD1 } from '@variance-authority/tribunal/testing';
470
+
471
+ const review = createReviewStore({
472
+ db: await createSqliteD1(),
473
+ bucket: createMemoryR2(),
474
+ project: 'todomvc',
475
+ });
476
+ ```
477
+
478
+ D1 *is* SQLite, so this adapter runs the package's schema, indexes, triggers,
479
+ `ON CONFLICT` clauses and ordering through `node:sqlite`.
480
+ `createMemoryR2().fail(…)` makes the bucket throw, allowing a host to exercise
481
+ its store-failure path.
482
+
483
+ That is also what an operator wants: their routes, their ingest, their retention
484
+ settings, in a plain `vitest` process, with no `wrangler`, no container and no
485
+ account.
486
+
487
+ ## Operational boundaries
488
+
489
+ The Worker relies on the platform's D1 transaction and `batch` semantics,
490
+ request and subrequest limits, object-size ceilings, quotas, and the behavior of
491
+ concurrent writes. Those are deployment conditions, not behavior this package
492
+ can configure or infer.
493
+
494
+ **Concurrency around run lineage is weaker than the SQLite backend's.** The
495
+ SQLite backend takes a write lock before checking whether a run id is
496
+ registered; two Workers cannot. A unique index still refuses a run id pointing
497
+ at two commits either way.
498
+
499
+ **A build's images are as large as the run kept.** Nothing here compresses,
500
+ resizes, or deduplicates across builds.
501
+
502
+ **A build cannot distinguish two images of one subject.** `ObservationRecord`
503
+ (the per-subject outcome a run reports — subject id, verdict, regions) carries
504
+ a subject and no label, so labelled baselines are writable through the store
505
+ and not reachable through the review path.
506
+
@@ -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.d.ts.map