@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.
- package/CHANGELOG.md +100 -0
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/dist/__fixtures__/review.d.ts +47 -0
- package/dist/__fixtures__/review.js +145 -0
- package/dist/__fixtures__/review.js.map +1 -0
- package/dist/bindings.d.ts +134 -0
- package/dist/bindings.js +88 -0
- package/dist/bindings.js.map +1 -0
- package/dist/changelog.d.ts +132 -0
- package/dist/changelog.js +129 -0
- package/dist/changelog.js.map +1 -0
- package/dist/history-queries.d.ts +82 -0
- package/dist/history-queries.js +200 -0
- package/dist/history-queries.js.map +1 -0
- package/dist/history-rows.d.ts +42 -0
- package/dist/history-rows.js +148 -0
- package/dist/history-rows.js.map +1 -0
- package/dist/history.d.ts +40 -0
- package/dist/history.js +186 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/migration-steps.d.ts +32 -0
- package/dist/migration-steps.js +303 -0
- package/dist/migration-steps.js.map +1 -0
- package/dist/migrations.d.ts +58 -0
- package/dist/migrations.js +283 -0
- package/dist/migrations.js.map +1 -0
- package/dist/next.d.ts +94 -0
- package/dist/next.js +44 -0
- package/dist/next.js.map +1 -0
- package/dist/node/bin.d.ts +120 -0
- package/dist/node/bin.js +337 -0
- package/dist/node/bin.js.map +1 -0
- package/dist/node/bucket.d.ts +58 -0
- package/dist/node/bucket.js +142 -0
- package/dist/node/bucket.js.map +1 -0
- package/dist/node/database.d.ts +33 -0
- package/dist/node/database.js +157 -0
- package/dist/node/database.js.map +1 -0
- package/dist/node/index.d.ts +43 -0
- package/dist/node/index.js +43 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/serve.d.ts +81 -0
- package/dist/node/serve.js +193 -0
- package/dist/node/serve.js.map +1 -0
- package/dist/node/ui-assets.d.ts +47 -0
- package/dist/node/ui-assets.js +60 -0
- package/dist/node/ui-assets.js.map +1 -0
- package/dist/review-ingest.d.ts +24 -0
- package/dist/review-ingest.js +184 -0
- package/dist/review-ingest.js.map +1 -0
- package/dist/review-read.d.ts +87 -0
- package/dist/review-read.js +361 -0
- package/dist/review-read.js.map +1 -0
- package/dist/review-rows.d.ts +30 -0
- package/dist/review-rows.js +67 -0
- package/dist/review-rows.js.map +1 -0
- package/dist/review-types.d.ts +414 -0
- package/dist/review-types.js +2 -0
- package/dist/review-types.js.map +1 -0
- package/dist/review-write.d.ts +35 -0
- package/dist/review-write.js +86 -0
- package/dist/review-write.js.map +1 -0
- package/dist/review.d.ts +53 -0
- package/dist/review.fixtures.d.ts +47 -0
- package/dist/review.fixtures.js +145 -0
- package/dist/review.fixtures.js.map +1 -0
- package/dist/review.js +245 -0
- package/dist/review.js.map +1 -0
- package/dist/schema.d.ts +36 -0
- package/dist/schema.js +39 -0
- package/dist/schema.js.map +1 -0
- package/dist/store.d.ts +58 -0
- package/dist/store.js +278 -0
- package/dist/store.js.map +1 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +92 -0
- package/dist/testing.js.map +1 -0
- package/dist/ui/__probe.d.ts +3 -0
- package/dist/ui/__probe.js +5 -0
- package/dist/ui/__probe.js.map +1 -0
- package/dist/ui/attribution.d.ts +46 -0
- package/dist/ui/attribution.js +79 -0
- package/dist/ui/attribution.js.map +1 -0
- package/dist/ui/because.d.ts +51 -0
- package/dist/ui/because.js +52 -0
- package/dist/ui/because.js.map +1 -0
- package/dist/ui/browser-entry.d.ts +2 -0
- package/dist/ui/browser-entry.js +34 -0
- package/dist/ui/browser-entry.js.map +1 -0
- package/dist/ui/builds.d.ts +43 -0
- package/dist/ui/builds.js +79 -0
- package/dist/ui/builds.js.map +1 -0
- package/dist/ui/carried.d.ts +73 -0
- package/dist/ui/carried.js +63 -0
- package/dist/ui/carried.js.map +1 -0
- package/dist/ui/cause.d.ts +73 -0
- package/dist/ui/cause.js +133 -0
- package/dist/ui/cause.js.map +1 -0
- package/dist/ui/change-story.d.ts +153 -0
- package/dist/ui/change-story.js +253 -0
- package/dist/ui/change-story.js.map +1 -0
- package/dist/ui/change.d.ts +51 -0
- package/dist/ui/change.js +125 -0
- package/dist/ui/change.js.map +1 -0
- package/dist/ui/client.d.ts +74 -0
- package/dist/ui/client.js +88 -0
- package/dist/ui/client.js.map +1 -0
- package/dist/ui/consumers.d.ts +80 -0
- package/dist/ui/consumers.js +125 -0
- package/dist/ui/consumers.js.map +1 -0
- package/dist/ui/control.d.ts +63 -0
- package/dist/ui/control.js +39 -0
- package/dist/ui/control.js.map +1 -0
- package/dist/ui/crossing.d.ts +55 -0
- package/dist/ui/crossing.js +74 -0
- package/dist/ui/crossing.js.map +1 -0
- package/dist/ui/declarations.d.ts +36 -0
- package/dist/ui/declarations.js +73 -0
- package/dist/ui/declarations.js.map +1 -0
- package/dist/ui/distance.d.ts +154 -0
- package/dist/ui/distance.js +180 -0
- package/dist/ui/distance.js.map +1 -0
- package/dist/ui/divergence.d.ts +40 -0
- package/dist/ui/divergence.js +176 -0
- package/dist/ui/divergence.js.map +1 -0
- package/dist/ui/docket.d.ts +39 -0
- package/dist/ui/docket.js +172 -0
- package/dist/ui/docket.js.map +1 -0
- package/dist/ui/findings.d.ts +42 -0
- package/dist/ui/findings.js +110 -0
- package/dist/ui/findings.js.map +1 -0
- package/dist/ui/foreseen.d.ts +55 -0
- package/dist/ui/foreseen.js +91 -0
- package/dist/ui/foreseen.js.map +1 -0
- package/dist/ui/glance.d.ts +101 -0
- package/dist/ui/glance.js +101 -0
- package/dist/ui/glance.js.map +1 -0
- package/dist/ui/grouping.d.ts +158 -0
- package/dist/ui/grouping.js +187 -0
- package/dist/ui/grouping.js.map +1 -0
- package/dist/ui/handed.d.ts +60 -0
- package/dist/ui/handed.js +122 -0
- package/dist/ui/handed.js.map +1 -0
- package/dist/ui/history.d.ts +88 -0
- package/dist/ui/history.js +170 -0
- package/dist/ui/history.js.map +1 -0
- package/dist/ui/holding.d.ts +84 -0
- package/dist/ui/holding.js +90 -0
- package/dist/ui/holding.js.map +1 -0
- package/dist/ui/impact.d.ts +31 -0
- package/dist/ui/impact.js +132 -0
- package/dist/ui/impact.js.map +1 -0
- package/dist/ui/index.d.ts +21 -0
- package/dist/ui/index.js +21 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/lead.d.ts +35 -0
- package/dist/ui/lead.js +34 -0
- package/dist/ui/lead.js.map +1 -0
- package/dist/ui/look.d.ts +109 -0
- package/dist/ui/look.js +248 -0
- package/dist/ui/look.js.map +1 -0
- package/dist/ui/mark.d.ts +11 -0
- package/dist/ui/mark.js +13 -0
- package/dist/ui/mark.js.map +1 -0
- package/dist/ui/moved.d.ts +121 -0
- package/dist/ui/moved.js +143 -0
- package/dist/ui/moved.js.map +1 -0
- package/dist/ui/navigation.d.ts +26 -0
- package/dist/ui/navigation.js +53 -0
- package/dist/ui/navigation.js.map +1 -0
- package/dist/ui/order.d.ts +113 -0
- package/dist/ui/order.js +226 -0
- package/dist/ui/order.js.map +1 -0
- package/dist/ui/origins.d.ts +42 -0
- package/dist/ui/origins.js +149 -0
- package/dist/ui/origins.js.map +1 -0
- package/dist/ui/outcome.d.ts +93 -0
- package/dist/ui/outcome.js +162 -0
- package/dist/ui/outcome.js.map +1 -0
- package/dist/ui/parted.d.ts +68 -0
- package/dist/ui/parted.js +101 -0
- package/dist/ui/parted.js.map +1 -0
- package/dist/ui/rail.d.ts +31 -0
- package/dist/ui/rail.js +21 -0
- package/dist/ui/rail.js.map +1 -0
- package/dist/ui/reach.d.ts +66 -0
- package/dist/ui/reach.js +185 -0
- package/dist/ui/reach.js.map +1 -0
- package/dist/ui/regions.d.ts +82 -0
- package/dist/ui/regions.js +67 -0
- package/dist/ui/regions.js.map +1 -0
- package/dist/ui/review.bundle.js +69 -0
- package/dist/ui/review.d.ts +97 -0
- package/dist/ui/review.js +39 -0
- package/dist/ui/review.js.map +1 -0
- package/dist/ui/root.d.ts +72 -0
- package/dist/ui/root.js +102 -0
- package/dist/ui/root.js.map +1 -0
- package/dist/ui/route.d.ts +95 -0
- package/dist/ui/route.js +114 -0
- package/dist/ui/route.js.map +1 -0
- package/dist/ui/run.d.ts +26 -0
- package/dist/ui/run.js +12 -0
- package/dist/ui/run.js.map +1 -0
- package/dist/ui/sense.d.ts +187 -0
- package/dist/ui/sense.js +222 -0
- package/dist/ui/sense.js.map +1 -0
- package/dist/ui/settled.d.ts +43 -0
- package/dist/ui/settled.js +59 -0
- package/dist/ui/settled.js.map +1 -0
- package/dist/ui/shell.d.ts +76 -0
- package/dist/ui/shell.js +65 -0
- package/dist/ui/shell.js.map +1 -0
- package/dist/ui/shift.d.ts +52 -0
- package/dist/ui/shift.js +98 -0
- package/dist/ui/shift.js.map +1 -0
- package/dist/ui/styles-docket.d.ts +18 -0
- package/dist/ui/styles-docket.js +281 -0
- package/dist/ui/styles-docket.js.map +1 -0
- package/dist/ui/styles-stage.d.ts +18 -0
- package/dist/ui/styles-stage.js +92 -0
- package/dist/ui/styles-stage.js.map +1 -0
- package/dist/ui/styles.d.ts +12 -0
- package/dist/ui/styles.js +482 -0
- package/dist/ui/styles.js.map +1 -0
- package/dist/ui/subject.d.ts +36 -0
- package/dist/ui/subject.js +138 -0
- package/dist/ui/subject.js.map +1 -0
- package/dist/ui/text.d.ts +129 -0
- package/dist/ui/text.js +164 -0
- package/dist/ui/text.js.map +1 -0
- package/dist/ui/variations.d.ts +37 -0
- package/dist/ui/variations.js +60 -0
- package/dist/ui/variations.js.map +1 -0
- package/dist/ui/viewer.d.ts +67 -0
- package/dist/ui/viewer.js +246 -0
- package/dist/ui/viewer.js.map +1 -0
- package/dist/worker-auth.d.ts +48 -0
- package/dist/worker-auth.js +74 -0
- package/dist/worker-auth.js.map +1 -0
- package/dist/worker-entry.d.ts +58 -0
- package/dist/worker-entry.js +36 -0
- package/dist/worker-entry.js.map +1 -0
- package/dist/worker-http.d.ts +39 -0
- package/dist/worker-http.js +111 -0
- package/dist/worker-http.js.map +1 -0
- package/dist/worker-input.d.ts +81 -0
- package/dist/worker-input.js +296 -0
- package/dist/worker-input.js.map +1 -0
- package/dist/worker.d.ts +91 -0
- package/dist/worker.js +287 -0
- package/dist/worker.js.map +1 -0
- package/mark.svg +30 -0
- package/migrations/0001_initial.sql +193 -0
- package/migrations/0002_changelog.sql +30 -0
- package/migrations/0003_accessibility.sql +8 -0
- package/migrations/0004_variations.sql +19 -0
- package/migrations/0005_reach.sql +26 -0
- package/migrations/0006_baseline-size.sql +7 -0
- package/migrations/0007_declarations.sql +7 -0
- package/migrations/0008_green-because.sql +7 -0
- package/migrations/0009_moved-bands.sql +6 -0
- package/migrations/0010_composition.sql +15 -0
- package/migrations/0011_movements.sql +21 -0
- package/migrations/0012_control-size.sql +6 -0
- package/package.json +110 -0
- 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
|
package/dist/bindings.js
ADDED
|
@@ -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
|