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