@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,148 @@
|
|
|
1
|
+
import { BANDS as FREQUENCY_BANDS, profileById, } from '@variance-authority/core';
|
|
2
|
+
import { BANDS, } from '@variance-authority/history';
|
|
3
|
+
/**
|
|
4
|
+
* An instant as a comparable number.
|
|
5
|
+
*
|
|
6
|
+
* Refuses rather than defaults. A row whose `at` cannot be parsed would land with
|
|
7
|
+
* a NULL ordering key, sort ahead of or behind everything depending on the query,
|
|
8
|
+
* and turn `12px → 20px` into `20px → 12px` — a confident sentence that is exactly
|
|
9
|
+
* backwards.
|
|
10
|
+
*/
|
|
11
|
+
export function instant(at, what) {
|
|
12
|
+
const parsed = Date.parse(at);
|
|
13
|
+
if (Number.isNaN(parsed)) {
|
|
14
|
+
throw new Error(`${what} carries "${at}", which is not an ISO-8601 instant`);
|
|
15
|
+
}
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
export function text(row, column, what) {
|
|
19
|
+
const value = row[column];
|
|
20
|
+
if (typeof value !== 'string') {
|
|
21
|
+
throw new Error(`${what} has a \`${column}\` that is not text: ${describe(value)}`);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function optionalText(row, column, what) {
|
|
26
|
+
const value = row[column];
|
|
27
|
+
if (value === null || value === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (typeof value !== 'string') {
|
|
30
|
+
throw new Error(`${what} has a \`${column}\` that is neither text nor null: ${describe(value)}`);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
export function number(row, column, what) {
|
|
35
|
+
const value = row[column];
|
|
36
|
+
if (typeof value === 'bigint')
|
|
37
|
+
return Number(value);
|
|
38
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
39
|
+
throw new Error(`${what} has a \`${column}\` that is not a number: ${describe(value)}`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function band(row, what) {
|
|
44
|
+
const value = text(row, 'band', what);
|
|
45
|
+
if (!BANDS.includes(value))
|
|
46
|
+
throw new Error(`${what} has an unknown band "${value}"`);
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
/** Checked against `core`'s table, so a new tier does not become a rejected row. */
|
|
50
|
+
function profile(row, what) {
|
|
51
|
+
const value = text(row, 'profile', what);
|
|
52
|
+
const known = profileById(value);
|
|
53
|
+
if (known === undefined)
|
|
54
|
+
throw new Error(`${what} has an unknown profile "${value}"`);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
export function toObservation(row) {
|
|
58
|
+
const what = 'a stored observation';
|
|
59
|
+
const file = optionalText(row, 'file', what);
|
|
60
|
+
return {
|
|
61
|
+
project: text(row, 'project', what),
|
|
62
|
+
subject: text(row, 'subject', what),
|
|
63
|
+
component: text(row, 'component', what),
|
|
64
|
+
band: band(row, what),
|
|
65
|
+
hash: text(row, 'hash', what),
|
|
66
|
+
profile: profile(row, what),
|
|
67
|
+
commit: text(row, 'commit', what),
|
|
68
|
+
run: text(row, 'run', what),
|
|
69
|
+
at: text(row, 'at', what),
|
|
70
|
+
accepted: number(row, 'accepted', what) !== 0,
|
|
71
|
+
...(file !== undefined ? { file } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function toRunRecord(row) {
|
|
75
|
+
const what = 'a stored run';
|
|
76
|
+
const swept = row['swept'];
|
|
77
|
+
return {
|
|
78
|
+
project: text(row, 'project', what),
|
|
79
|
+
run: text(row, 'run', what),
|
|
80
|
+
commit: text(row, 'commit', what),
|
|
81
|
+
profile: profile(row, what),
|
|
82
|
+
at: text(row, 'at', what),
|
|
83
|
+
// Null stays absent. A run recorded before the column existed never said what
|
|
84
|
+
// it examined, and "did not sweep" is a claim — one that would make an old
|
|
85
|
+
// history look like a suite nobody ever swept.
|
|
86
|
+
...(swept === null || swept === undefined ? {} : { swept: number(row, 'swept', what) !== 0 }),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* One occurrence of a subject failing to read the same way twice.
|
|
91
|
+
*
|
|
92
|
+
* Transcribed from `server/sqlite-rows.ts`, nullability included: a component
|
|
93
|
+
* that could not be named comes back absent rather than as an empty string,
|
|
94
|
+
* because empty would read as a component whose name is nothing.
|
|
95
|
+
*/
|
|
96
|
+
export function toInstability(row) {
|
|
97
|
+
const what = 'a stored instability';
|
|
98
|
+
const component = optionalText(row, 'component', what);
|
|
99
|
+
const stored = optionalText(row, 'band', what);
|
|
100
|
+
const absorbedBy = optionalText(row, 'absorbed_by', what);
|
|
101
|
+
return {
|
|
102
|
+
project: text(row, 'project', what),
|
|
103
|
+
subject: text(row, 'subject', what),
|
|
104
|
+
...(component !== undefined ? { component } : {}),
|
|
105
|
+
...(stored !== undefined ? { band: frequencyBand(stored, what) } : {}),
|
|
106
|
+
profile: profile(row, what),
|
|
107
|
+
commit: text(row, 'commit', what),
|
|
108
|
+
run: text(row, 'run', what),
|
|
109
|
+
at: text(row, 'at', what),
|
|
110
|
+
...(absorbedBy !== undefined ? { absorbedBy } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** A frequency band — how often that kind of thing changes — not a hash band. */
|
|
114
|
+
function frequencyBand(value, what) {
|
|
115
|
+
if (!FREQUENCY_BANDS.includes(value)) {
|
|
116
|
+
throw new Error(`${what} has an unknown frequency band "${value}"`);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
export function toApproval(row) {
|
|
121
|
+
const what = 'a stored approval';
|
|
122
|
+
const by = optionalText(row, 'approver', what);
|
|
123
|
+
return {
|
|
124
|
+
project: text(row, 'project', what),
|
|
125
|
+
subject: text(row, 'subject', what),
|
|
126
|
+
run: text(row, 'run', what),
|
|
127
|
+
at: text(row, 'at', what),
|
|
128
|
+
...(by !== undefined ? { by } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function toTokenValue(row) {
|
|
132
|
+
const what = 'a stored token value';
|
|
133
|
+
return {
|
|
134
|
+
project: text(row, 'project', what),
|
|
135
|
+
token: text(row, 'token', what),
|
|
136
|
+
value: text(row, 'value', what),
|
|
137
|
+
commit: text(row, 'commit', what),
|
|
138
|
+
at: text(row, 'at', what),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function describe(value) {
|
|
142
|
+
if (value === undefined)
|
|
143
|
+
return 'absent';
|
|
144
|
+
if (value === null)
|
|
145
|
+
return 'null';
|
|
146
|
+
return `${typeof value} (${String(value).slice(0, 60)})`;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=history-rows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-rows.js","sourceRoot":"","sources":["../src/history-rows.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BANDS as FREQUENCY_BANDS,\n profileById,\n type Digest,\n type ProfileId,\n} from '@variance-authority/core';\nimport {\n BANDS,\n type Approval,\n type Band,\n type FrequencyBand,\n type Instability,\n type Observation,\n type RunRecord,\n type TokenValue,\n} from '@variance-authority/history';\n\n/**\n * Reading a row back is validation, not a cast.\n *\n * SQLite is typed per value rather than per column, and `STRICT` only constrains\n * what this build writes. A row written by a future version, or by a person at a\n * `wrangler d1 execute` prompt, arrives here as whatever it is; casting it would\n * let a number become a component name and a NULL become the string \"null\" three\n * layers later.\n *\n * That argument is the whole of this file, and it is why it is a file: every\n * reader below is one refusal, and the same handful of them is used by the append\n * path, the filters, the slices and the reach query alike. `instant` sits with\n * them because it is the same refusal applied to a bound rather than to a column.\n * Nothing here knows any SQL, and [`history.ts`](./history.ts) knows nothing\n * about what a stored value is allowed to be.\n */\n\nexport type Row = Record<string, unknown>;\n\n/**\n * An instant as a comparable number.\n *\n * Refuses rather than defaults. A row whose `at` cannot be parsed would land with\n * a NULL ordering key, sort ahead of or behind everything depending on the query,\n * and turn `12px → 20px` into `20px → 12px` — a confident sentence that is exactly\n * backwards.\n */\nexport function instant(at: string, what: string): number {\n const parsed = Date.parse(at);\n if (Number.isNaN(parsed)) {\n throw new Error(`${what} carries \"${at}\", which is not an ISO-8601 instant`);\n }\n return parsed;\n}\n\nexport function text(row: Row, column: string, what: string): string {\n const value = row[column];\n if (typeof value !== 'string') {\n throw new Error(`${what} has a \\`${column}\\` that is not text: ${describe(value)}`);\n }\n return value;\n}\n\nfunction optionalText(row: Row, column: string, what: string): string | undefined {\n const value = row[column];\n if (value === null || value === undefined) return undefined;\n if (typeof value !== 'string') {\n throw new Error(`${what} has a \\`${column}\\` that is neither text nor null: ${describe(value)}`);\n }\n return value;\n}\n\nexport function number(row: Row, column: string, what: string): number {\n const value = row[column];\n if (typeof value === 'bigint') return Number(value);\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n throw new Error(`${what} has a \\`${column}\\` that is not a number: ${describe(value)}`);\n }\n return value;\n}\n\nfunction band(row: Row, what: string): Band {\n const value = text(row, 'band', what);\n if (!BANDS.includes(value as Band)) throw new Error(`${what} has an unknown band \"${value}\"`);\n return value as Band;\n}\n\n/** Checked against `core`'s table, so a new tier does not become a rejected row. */\nfunction profile(row: Row, what: string): ProfileId {\n const value = text(row, 'profile', what);\n const known: unknown = profileById(value as ProfileId);\n if (known === undefined) throw new Error(`${what} has an unknown profile \"${value}\"`);\n return value as ProfileId;\n}\n\nexport function toObservation(row: Row): Observation {\n const what = 'a stored observation';\n const file = optionalText(row, 'file', what);\n\n return {\n project: text(row, 'project', what),\n subject: text(row, 'subject', what),\n component: text(row, 'component', what),\n band: band(row, what),\n hash: text(row, 'hash', what) as Digest,\n profile: profile(row, what),\n commit: text(row, 'commit', what),\n run: text(row, 'run', what),\n at: text(row, 'at', what),\n accepted: number(row, 'accepted', what) !== 0,\n ...(file !== undefined ? { file } : {}),\n };\n}\n\nexport function toRunRecord(row: Row): RunRecord {\n const what = 'a stored run';\n const swept = row['swept'];\n\n return {\n project: text(row, 'project', what),\n run: text(row, 'run', what),\n commit: text(row, 'commit', what),\n profile: profile(row, what),\n at: text(row, 'at', what),\n // Null stays absent. A run recorded before the column existed never said what\n // it examined, and \"did not sweep\" is a claim — one that would make an old\n // history look like a suite nobody ever swept.\n ...(swept === null || swept === undefined ? {} : { swept: number(row, 'swept', what) !== 0 }),\n };\n}\n\n/**\n * One occurrence of a subject failing to read the same way twice.\n *\n * Transcribed from `server/sqlite-rows.ts`, nullability included: a component\n * that could not be named comes back absent rather than as an empty string,\n * because empty would read as a component whose name is nothing.\n */\nexport function toInstability(row: Row): Instability {\n const what = 'a stored instability';\n const component = optionalText(row, 'component', what);\n const stored = optionalText(row, 'band', what);\n const absorbedBy = optionalText(row, 'absorbed_by', what);\n\n return {\n project: text(row, 'project', what),\n subject: text(row, 'subject', what),\n ...(component !== undefined ? { component } : {}),\n ...(stored !== undefined ? { band: frequencyBand(stored, what) } : {}),\n profile: profile(row, what),\n commit: text(row, 'commit', what),\n run: text(row, 'run', what),\n at: text(row, 'at', what),\n ...(absorbedBy !== undefined ? { absorbedBy } : {}),\n };\n}\n\n/** A frequency band — how often that kind of thing changes — not a hash band. */\nfunction frequencyBand(value: string, what: string): FrequencyBand {\n if (!FREQUENCY_BANDS.includes(value as FrequencyBand)) {\n throw new Error(`${what} has an unknown frequency band \"${value}\"`);\n }\n return value as FrequencyBand;\n}\n\nexport function toApproval(row: Row): Approval {\n const what = 'a stored approval';\n const by = optionalText(row, 'approver', what);\n\n return {\n project: text(row, 'project', what),\n subject: text(row, 'subject', what),\n run: text(row, 'run', what),\n at: text(row, 'at', what),\n ...(by !== undefined ? { by } : {}),\n };\n}\n\nexport function toTokenValue(row: Row): TokenValue {\n const what = 'a stored token value';\n return {\n project: text(row, 'project', what),\n token: text(row, 'token', what),\n value: text(row, 'value', what),\n commit: text(row, 'commit', what),\n at: text(row, 'at', what),\n };\n}\n\nfunction describe(value: unknown): string {\n if (value === undefined) return 'absent';\n if (value === null) return 'null';\n return `${typeof value} (${String(value).slice(0, 60)})`;\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type HistoryBackend } from '@variance-authority/server';
|
|
2
|
+
import { type D1Like } from './bindings.js';
|
|
3
|
+
/**
|
|
4
|
+
* The history record, on D1.
|
|
5
|
+
*
|
|
6
|
+
* A second implementation of `HistoryBackend`, and deliberately a *transcription*
|
|
7
|
+
* of [`server/backend-sqlite.ts`](../../server/src/backend-sqlite.ts) rather than
|
|
8
|
+
* a fresh design. The seam exists so that replacing the engine changes one file
|
|
9
|
+
* and not what a row means; the way to honour that is for the two files to be
|
|
10
|
+
* boringly alike — same filters, same limit arithmetic, same correlated subquery
|
|
11
|
+
* for `arrived`, same refusal to cast a row it read back.
|
|
12
|
+
*
|
|
13
|
+
* **No arithmetic happens here.** Churn, journeys and reach are computed by
|
|
14
|
+
* `churnFrom`/`journeyFrom`/`reachFrom` in `@variance-authority/server`, over the
|
|
15
|
+
* rows this returns. That is what stops two backends disagreeing about what a
|
|
16
|
+
* number means, and it is why this file has no idea what a rate is.
|
|
17
|
+
*
|
|
18
|
+
* ## The two places D1 is not SQLite-in-a-process
|
|
19
|
+
*
|
|
20
|
+
* **No `BEGIN IMMEDIATE`.** Atomicity comes from `batch`, which D1 documents as
|
|
21
|
+
* one implicit transaction that rolls back entirely on failure. Every write here
|
|
22
|
+
* is one batch, so a run and its rows still land together or not at all.
|
|
23
|
+
*
|
|
24
|
+
* **No serialization around a read-then-write.** The SQLite backend takes the
|
|
25
|
+
* write lock before checking whether a run id is already registered; two Workers
|
|
26
|
+
* cannot. So the check is not what enforces lineage — the `WHERE NOT EXISTS`
|
|
27
|
+
* guard on the insert is, and the unique index behind it. The read survives only
|
|
28
|
+
* to produce a sentence naming both commits, because a caller who gets
|
|
29
|
+
* `UNIQUE constraint failed` has to come and read this file to find out what
|
|
30
|
+
* happened.
|
|
31
|
+
*
|
|
32
|
+
* ## What is next door
|
|
33
|
+
*
|
|
34
|
+
* Reading a stored value back is [`history-rows.ts`](./history-rows.ts), and it
|
|
35
|
+
* is a separate file for the same reason no arithmetic happens here: what a row
|
|
36
|
+
* is allowed to be is not a property of any one query, and this file knows
|
|
37
|
+
* nothing about it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createD1Backend(db: D1Like): HistoryBackend;
|
|
40
|
+
//# sourceMappingURL=history.d.ts.map
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { HistoryWriteConflict, } from '@variance-authority/server';
|
|
2
|
+
import { requireD1 } from './bindings.js';
|
|
3
|
+
import { areaFilter, componentFilter, currentRows, currentTokenRows, reachRows, slice, subjectFilter, tokenFilter, windowFilter, } from './history-queries.js';
|
|
4
|
+
import { instant, text, toApproval, toInstability, toObservation, toRunRecord, toTokenValue, } from './history-rows.js';
|
|
5
|
+
/**
|
|
6
|
+
* The history record, on D1.
|
|
7
|
+
*
|
|
8
|
+
* A second implementation of `HistoryBackend`, and deliberately a *transcription*
|
|
9
|
+
* of [`server/backend-sqlite.ts`](../../server/src/backend-sqlite.ts) rather than
|
|
10
|
+
* a fresh design. The seam exists so that replacing the engine changes one file
|
|
11
|
+
* and not what a row means; the way to honour that is for the two files to be
|
|
12
|
+
* boringly alike — same filters, same limit arithmetic, same correlated subquery
|
|
13
|
+
* for `arrived`, same refusal to cast a row it read back.
|
|
14
|
+
*
|
|
15
|
+
* **No arithmetic happens here.** Churn, journeys and reach are computed by
|
|
16
|
+
* `churnFrom`/`journeyFrom`/`reachFrom` in `@variance-authority/server`, over the
|
|
17
|
+
* rows this returns. That is what stops two backends disagreeing about what a
|
|
18
|
+
* number means, and it is why this file has no idea what a rate is.
|
|
19
|
+
*
|
|
20
|
+
* ## The two places D1 is not SQLite-in-a-process
|
|
21
|
+
*
|
|
22
|
+
* **No `BEGIN IMMEDIATE`.** Atomicity comes from `batch`, which D1 documents as
|
|
23
|
+
* one implicit transaction that rolls back entirely on failure. Every write here
|
|
24
|
+
* is one batch, so a run and its rows still land together or not at all.
|
|
25
|
+
*
|
|
26
|
+
* **No serialization around a read-then-write.** The SQLite backend takes the
|
|
27
|
+
* write lock before checking whether a run id is already registered; two Workers
|
|
28
|
+
* cannot. So the check is not what enforces lineage — the `WHERE NOT EXISTS`
|
|
29
|
+
* guard on the insert is, and the unique index behind it. The read survives only
|
|
30
|
+
* to produce a sentence naming both commits, because a caller who gets
|
|
31
|
+
* `UNIQUE constraint failed` has to come and read this file to find out what
|
|
32
|
+
* happened.
|
|
33
|
+
*
|
|
34
|
+
* ## What is next door
|
|
35
|
+
*
|
|
36
|
+
* Reading a stored value back is [`history-rows.ts`](./history-rows.ts), and it
|
|
37
|
+
* is a separate file for the same reason no arithmetic happens here: what a row
|
|
38
|
+
* is allowed to be is not a property of any one query, and this file knows
|
|
39
|
+
* nothing about it.
|
|
40
|
+
*/
|
|
41
|
+
export function createD1Backend(db) {
|
|
42
|
+
requireD1(db);
|
|
43
|
+
return {
|
|
44
|
+
async append(run, observations, tokens, instabilities = []) {
|
|
45
|
+
const registered = await registeredCommit(db, run);
|
|
46
|
+
if (registered !== null && registered !== run.commit)
|
|
47
|
+
throw conflict(run, registered);
|
|
48
|
+
const shipped = carriesAnApproval(observations);
|
|
49
|
+
const statements = [
|
|
50
|
+
// One statement, and it is the lineage rule rather than a convenience.
|
|
51
|
+
// If a row already exists for this `(project, run, profile)` with *this*
|
|
52
|
+
// commit, the `NOT EXISTS` is false and nothing is inserted — re-appending
|
|
53
|
+
// a run is legitimate, because a caller whose components were approved
|
|
54
|
+
// separately writes twice, and a run counted twice halves every rate
|
|
55
|
+
// derived from it forever.
|
|
56
|
+
//
|
|
57
|
+
// If a row exists with a *different* commit, the `NOT EXISTS` is true, the
|
|
58
|
+
// insert proceeds, and `runs_identity` refuses it — which aborts the whole
|
|
59
|
+
// batch. That is the only part of this that holds under two concurrent
|
|
60
|
+
// Workers, and it is why the check above is a message rather than a guard.
|
|
61
|
+
db
|
|
62
|
+
.prepare(`INSERT INTO runs (project, run, "commit", profile, at, at_ms, swept)
|
|
63
|
+
SELECT ?, ?, ?, ?, ?, ?, ?
|
|
64
|
+
WHERE NOT EXISTS (
|
|
65
|
+
SELECT 1 FROM runs
|
|
66
|
+
WHERE project = ? AND run = ? AND profile = ? AND "commit" = ?)`)
|
|
67
|
+
.bind(run.project, run.run, run.commit, run.profile, run.at, instant(run.at, 'run'),
|
|
68
|
+
// NULL when the caller did not say. Transcribed with its reason: a
|
|
69
|
+
// run stored as having examined nothing puts a denominator under a
|
|
70
|
+
// flake rate that nobody measured.
|
|
71
|
+
run.swept === undefined ? null : run.swept ? 1 : 0, run.project, run.run, run.profile, run.commit),
|
|
72
|
+
];
|
|
73
|
+
for (const row of observations) {
|
|
74
|
+
statements.push(db
|
|
75
|
+
.prepare(`INSERT INTO observations
|
|
76
|
+
(project, subject, component, band, hash, profile, "commit", run, at, at_ms,
|
|
77
|
+
accepted, file)
|
|
78
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
79
|
+
.bind(row.project, row.subject, row.component, row.band, row.hash, row.profile, row.commit, row.run, row.at, instant(row.at, 'observation'), row.accepted ? 1 : 0, row.file ?? null));
|
|
80
|
+
}
|
|
81
|
+
for (const row of tokens) {
|
|
82
|
+
statements.push(db
|
|
83
|
+
.prepare(`INSERT INTO token_values (project, token, value, "commit", at, at_ms, run, accepted)
|
|
84
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
85
|
+
.bind(row.project, row.token, row.value, row.commit, row.at, instant(row.at, 'token value'), run.run, shipped ? 1 : 0));
|
|
86
|
+
}
|
|
87
|
+
for (const row of instabilities) {
|
|
88
|
+
statements.push(db
|
|
89
|
+
.prepare(`INSERT INTO instabilities
|
|
90
|
+
(project, subject, component, band, profile, "commit", run, at, at_ms, absorbed_by)
|
|
91
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
92
|
+
.bind(row.project, row.subject,
|
|
93
|
+
// Null, not the empty string: readings that could not be resolved
|
|
94
|
+
// to a component named nobody, which is not the same as the
|
|
95
|
+
// disagreement belonging to no component.
|
|
96
|
+
row.component ?? null, row.band ?? null, row.profile, row.commit, row.run, row.at, instant(row.at, 'instability'), row.absorbedBy ?? null));
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
await db.batch(statements);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// The guard above fired between the check and the write. Re-read so the
|
|
103
|
+
// operator gets the two commits rather than an index name.
|
|
104
|
+
const current = await registeredCommit(db, run).catch(() => null);
|
|
105
|
+
if (current !== null && current !== run.commit)
|
|
106
|
+
throw conflict(run, current);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
async appendApprovals(approvals) {
|
|
111
|
+
if (approvals.length === 0)
|
|
112
|
+
return;
|
|
113
|
+
// `OR IGNORE` against `approvals_identity`, transcribed: approving twice is
|
|
114
|
+
// approving once, and a refusal would turn a second click into a failure.
|
|
115
|
+
await db.batch(approvals.map((approval) => db
|
|
116
|
+
.prepare(`INSERT OR IGNORE INTO approvals (project, subject, run, at, at_ms, approver)
|
|
117
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
118
|
+
.bind(approval.project, approval.subject, approval.run, approval.at, instant(approval.at, 'approval'), approval.by ?? null)));
|
|
119
|
+
},
|
|
120
|
+
async approvalsOf(query) {
|
|
121
|
+
return slice(db, 'approvals', windowFilter(query), query.limit, toApproval);
|
|
122
|
+
},
|
|
123
|
+
async currentOf(query) {
|
|
124
|
+
return currentRows(db, query);
|
|
125
|
+
},
|
|
126
|
+
async currentTokens(query) {
|
|
127
|
+
return currentTokenRows(db, query);
|
|
128
|
+
},
|
|
129
|
+
async lastObservation(query) {
|
|
130
|
+
const filter = areaFilter(query);
|
|
131
|
+
const row = await db
|
|
132
|
+
.prepare(`SELECT * FROM observations WHERE 1 = 1${filter.sql} ORDER BY at_ms DESC, rowid DESC LIMIT 1`)
|
|
133
|
+
.bind(...filter.params)
|
|
134
|
+
.first();
|
|
135
|
+
return row === null ? null : toObservation(row);
|
|
136
|
+
},
|
|
137
|
+
async runsIn(query) {
|
|
138
|
+
return slice(db, 'runs', windowFilter(query), query.limit, toRunRecord);
|
|
139
|
+
},
|
|
140
|
+
async observationsOf(query) {
|
|
141
|
+
return slice(db, 'observations', componentFilter(query), query.limit, toObservation);
|
|
142
|
+
},
|
|
143
|
+
async instabilitiesOf(query) {
|
|
144
|
+
return slice(db, 'instabilities', subjectFilter(query), query.limit, toInstability);
|
|
145
|
+
},
|
|
146
|
+
async valuesOf(query) {
|
|
147
|
+
return slice(db, 'token_values', tokenFilter(query), query.limit, toTokenValue);
|
|
148
|
+
},
|
|
149
|
+
async reachOf(query) {
|
|
150
|
+
return reachRows(db, query);
|
|
151
|
+
},
|
|
152
|
+
/**
|
|
153
|
+
* Nothing to close.
|
|
154
|
+
*
|
|
155
|
+
* A binding is not a handle this code opened, and a Worker that closed one
|
|
156
|
+
* would break every other request sharing the isolate. Present because the
|
|
157
|
+
* interface has it, and empty for the same reason a stateless service has no
|
|
158
|
+
* shutdown.
|
|
159
|
+
*/
|
|
160
|
+
async close() { },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async function registeredCommit(db, run) {
|
|
164
|
+
const row = await db
|
|
165
|
+
.prepare('SELECT "commit" FROM runs WHERE project = ? AND run = ? AND profile = ?')
|
|
166
|
+
.bind(run.project, run.run, run.profile)
|
|
167
|
+
.first();
|
|
168
|
+
return row === null ? null : text(row, 'commit', 'a stored run');
|
|
169
|
+
}
|
|
170
|
+
function conflict(run, recorded) {
|
|
171
|
+
return new HistoryWriteConflict(`run "${run.run}" of project "${run.project}" under ${run.profile} is already recorded ` +
|
|
172
|
+
`at commit ${recorded}; this write claims ${run.commit}. A run id that points at two ` +
|
|
173
|
+
'commits makes every question asked by lineage unanswerable, so the write is refused ' +
|
|
174
|
+
'rather than appended alongside it');
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Whether the write itself already carried an approval.
|
|
178
|
+
*
|
|
179
|
+
* Transcribed from the SQLite backend, and provenance about the write in both.
|
|
180
|
+
* Which values a journey is made of is decided by `journeyFrom`, which both
|
|
181
|
+
* backends share and which asks the approvals table.
|
|
182
|
+
*/
|
|
183
|
+
function carriesAnApproval(observations) {
|
|
184
|
+
return observations.length === 0 || observations.some((row) => row.accepted);
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.js","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n Approval,\n Instability,\n Observation,\n RunRecord,\n TokenValue,\n} from '@variance-authority/history';\nimport {\n HistoryWriteConflict,\n type HistoryBackend,\n type ReachRows,\n type Slice,\n} from '@variance-authority/server';\nimport { requireD1, type D1Like, type D1PreparedLike } from './bindings.js';\nimport {\n areaFilter,\n componentFilter,\n currentRows,\n currentTokenRows,\n reachRows,\n slice,\n subjectFilter,\n tokenFilter,\n windowFilter,\n} from './history-queries.js';\nimport {\n instant,\n text,\n toApproval,\n toInstability,\n toObservation,\n toRunRecord,\n toTokenValue,\n type Row,\n} from './history-rows.js';\n\n/**\n * The history record, on D1.\n *\n * A second implementation of `HistoryBackend`, and deliberately a *transcription*\n * of [`server/backend-sqlite.ts`](../../server/src/backend-sqlite.ts) rather than\n * a fresh design. The seam exists so that replacing the engine changes one file\n * and not what a row means; the way to honour that is for the two files to be\n * boringly alike — same filters, same limit arithmetic, same correlated subquery\n * for `arrived`, same refusal to cast a row it read back.\n *\n * **No arithmetic happens here.** Churn, journeys and reach are computed by\n * `churnFrom`/`journeyFrom`/`reachFrom` in `@variance-authority/server`, over the\n * rows this returns. That is what stops two backends disagreeing about what a\n * number means, and it is why this file has no idea what a rate is.\n *\n * ## The two places D1 is not SQLite-in-a-process\n *\n * **No `BEGIN IMMEDIATE`.** Atomicity comes from `batch`, which D1 documents as\n * one implicit transaction that rolls back entirely on failure. Every write here\n * is one batch, so a run and its rows still land together or not at all.\n *\n * **No serialization around a read-then-write.** The SQLite backend takes the\n * write lock before checking whether a run id is already registered; two Workers\n * cannot. So the check is not what enforces lineage — the `WHERE NOT EXISTS`\n * guard on the insert is, and the unique index behind it. The read survives only\n * to produce a sentence naming both commits, because a caller who gets\n * `UNIQUE constraint failed` has to come and read this file to find out what\n * happened.\n *\n * ## What is next door\n *\n * Reading a stored value back is [`history-rows.ts`](./history-rows.ts), and it\n * is a separate file for the same reason no arithmetic happens here: what a row\n * is allowed to be is not a property of any one query, and this file knows\n * nothing about it.\n */\n\nexport function createD1Backend(db: D1Like): HistoryBackend {\n requireD1(db);\n\n return {\n async append(run, observations, tokens, instabilities = []): Promise<void> {\n const registered = await registeredCommit(db, run);\n if (registered !== null && registered !== run.commit) throw conflict(run, registered);\n\n const shipped = carriesAnApproval(observations);\n const statements: D1PreparedLike[] = [\n // One statement, and it is the lineage rule rather than a convenience.\n // If a row already exists for this `(project, run, profile)` with *this*\n // commit, the `NOT EXISTS` is false and nothing is inserted — re-appending\n // a run is legitimate, because a caller whose components were approved\n // separately writes twice, and a run counted twice halves every rate\n // derived from it forever.\n //\n // If a row exists with a *different* commit, the `NOT EXISTS` is true, the\n // insert proceeds, and `runs_identity` refuses it — which aborts the whole\n // batch. That is the only part of this that holds under two concurrent\n // Workers, and it is why the check above is a message rather than a guard.\n db\n .prepare(\n `INSERT INTO runs (project, run, \"commit\", profile, at, at_ms, swept)\n SELECT ?, ?, ?, ?, ?, ?, ?\n WHERE NOT EXISTS (\n SELECT 1 FROM runs\n WHERE project = ? AND run = ? AND profile = ? AND \"commit\" = ?)`,\n )\n .bind(\n run.project,\n run.run,\n run.commit,\n run.profile,\n run.at,\n instant(run.at, 'run'),\n // NULL when the caller did not say. Transcribed with its reason: a\n // run stored as having examined nothing puts a denominator under a\n // flake rate that nobody measured.\n run.swept === undefined ? null : run.swept ? 1 : 0,\n run.project,\n run.run,\n run.profile,\n run.commit,\n ),\n ];\n\n for (const row of observations) {\n statements.push(\n db\n .prepare(\n `INSERT INTO observations\n (project, subject, component, band, hash, profile, \"commit\", run, at, at_ms,\n accepted, file)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .bind(\n row.project,\n row.subject,\n row.component,\n row.band,\n row.hash,\n row.profile,\n row.commit,\n row.run,\n row.at,\n instant(row.at, 'observation'),\n row.accepted ? 1 : 0,\n row.file ?? null,\n ),\n );\n }\n\n for (const row of tokens) {\n statements.push(\n db\n .prepare(\n `INSERT INTO token_values (project, token, value, \"commit\", at, at_ms, run, accepted)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .bind(\n row.project,\n row.token,\n row.value,\n row.commit,\n row.at,\n instant(row.at, 'token value'),\n run.run,\n shipped ? 1 : 0,\n ),\n );\n }\n\n for (const row of instabilities) {\n statements.push(\n db\n .prepare(\n `INSERT INTO instabilities\n (project, subject, component, band, profile, \"commit\", run, at, at_ms, absorbed_by)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .bind(\n row.project,\n row.subject,\n // Null, not the empty string: readings that could not be resolved\n // to a component named nobody, which is not the same as the\n // disagreement belonging to no component.\n row.component ?? null,\n row.band ?? null,\n row.profile,\n row.commit,\n row.run,\n row.at,\n instant(row.at, 'instability'),\n row.absorbedBy ?? null,\n ),\n );\n }\n\n try {\n await db.batch(statements);\n } catch (error) {\n // The guard above fired between the check and the write. Re-read so the\n // operator gets the two commits rather than an index name.\n const current = await registeredCommit(db, run).catch(() => null);\n if (current !== null && current !== run.commit) throw conflict(run, current);\n throw error;\n }\n },\n\n async appendApprovals(approvals): Promise<void> {\n if (approvals.length === 0) return;\n\n // `OR IGNORE` against `approvals_identity`, transcribed: approving twice is\n // approving once, and a refusal would turn a second click into a failure.\n await db.batch(\n approvals.map((approval) =>\n db\n .prepare(\n `INSERT OR IGNORE INTO approvals (project, subject, run, at, at_ms, approver)\n VALUES (?, ?, ?, ?, ?, ?)`,\n )\n .bind(\n approval.project,\n approval.subject,\n approval.run,\n approval.at,\n instant(approval.at, 'approval'),\n approval.by ?? null,\n ),\n ),\n );\n },\n\n async approvalsOf(query): Promise<Slice<Approval>> {\n return slice(db, 'approvals', windowFilter(query), query.limit, toApproval);\n },\n\n async currentOf(query): Promise<readonly Observation[]> {\n return currentRows(db, query);\n },\n\n async currentTokens(query): Promise<readonly TokenValue[]> {\n return currentTokenRows(db, query);\n },\n\n async lastObservation(query): Promise<Observation | null> {\n const filter = areaFilter(query);\n const row = await db\n .prepare(\n `SELECT * FROM observations WHERE 1 = 1${filter.sql} ORDER BY at_ms DESC, rowid DESC LIMIT 1`,\n )\n .bind(...filter.params)\n .first<Row>();\n\n return row === null ? null : toObservation(row);\n },\n\n async runsIn(query): Promise<Slice<RunRecord>> {\n return slice(db, 'runs', windowFilter(query), query.limit, toRunRecord);\n },\n\n async observationsOf(query): Promise<Slice<Observation>> {\n return slice(db, 'observations', componentFilter(query), query.limit, toObservation);\n },\n\n async instabilitiesOf(query): Promise<Slice<Instability>> {\n return slice(db, 'instabilities', subjectFilter(query), query.limit, toInstability);\n },\n\n async valuesOf(query): Promise<Slice<TokenValue>> {\n return slice(db, 'token_values', tokenFilter(query), query.limit, toTokenValue);\n },\n\n async reachOf(query): Promise<ReachRows> {\n return reachRows(db, query);\n },\n\n /**\n * Nothing to close.\n *\n * A binding is not a handle this code opened, and a Worker that closed one\n * would break every other request sharing the isolate. Present because the\n * interface has it, and empty for the same reason a stateless service has no\n * shutdown.\n */\n async close(): Promise<void> {},\n };\n}\n\nasync function registeredCommit(db: D1Like, run: RunRecord): Promise<string | null> {\n const row = await db\n .prepare('SELECT \"commit\" FROM runs WHERE project = ? AND run = ? AND profile = ?')\n .bind(run.project, run.run, run.profile)\n .first<Row>();\n\n return row === null ? null : text(row, 'commit', 'a stored run');\n}\n\nfunction conflict(run: RunRecord, recorded: string): HistoryWriteConflict {\n return new HistoryWriteConflict(\n `run \"${run.run}\" of project \"${run.project}\" under ${run.profile} is already recorded ` +\n `at commit ${recorded}; this write claims ${run.commit}. A run id that points at two ` +\n 'commits makes every question asked by lineage unanswerable, so the write is refused ' +\n 'rather than appended alongside it',\n );\n}\n\n/**\n * Whether the write itself already carried an approval.\n *\n * Transcribed from the SQLite backend, and provenance about the write in both.\n * Which values a journey is made of is decided by `journeyFrom`, which both\n * backends share and which asks the approvals table.\n */\nfunction carriesAnApproval(observations: readonly Observation[]): boolean {\n return observations.length === 0 || observations.some((row) => row.accepted);\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract half — what a deployment is made of, with nothing that runs.
|
|
3
|
+
*
|
|
4
|
+
* The entrypoint an operator imports to declare their bindings and apply their
|
|
5
|
+
* schema. Everything that touches a request is behind `./store`, `./history`,
|
|
6
|
+
* `./review` and `./worker`, so a build script that only needs the SQL does not
|
|
7
|
+
* pull in the router, and a Worker that only serves baselines does not pull in
|
|
8
|
+
* React.
|
|
9
|
+
*/
|
|
10
|
+
export type { D1Like, D1PreparedLike, D1Value, R2Like, R2ObjectLike, TribunalBindings, } from './bindings.js';
|
|
11
|
+
export { base64Of, bytesOf } from './bindings.js';
|
|
12
|
+
export { INITIAL_VERSION, MIGRATIONS, SCHEMA, SCHEMA_VERSION, applySchema } from './schema.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract half — what a deployment is made of, with nothing that runs.
|
|
3
|
+
*
|
|
4
|
+
* The entrypoint an operator imports to declare their bindings and apply their
|
|
5
|
+
* schema. Everything that touches a request is behind `./store`, `./history`,
|
|
6
|
+
* `./review` and `./worker`, so a build script that only needs the SQL does not
|
|
7
|
+
* pull in the router, and a Worker that only serves baselines does not pull in
|
|
8
|
+
* React.
|
|
9
|
+
*/
|
|
10
|
+
export { base64Of, bytesOf } from './bindings.js';
|
|
11
|
+
export { INITIAL_VERSION, MIGRATIONS, SCHEMA, SCHEMA_VERSION, applySchema } from './schema.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC","sourcesContent":["/**\n * The contract half — what a deployment is made of, with nothing that runs.\n *\n * The entrypoint an operator imports to declare their bindings and apply their\n * schema. Everything that touches a request is behind `./store`, `./history`,\n * `./review` and `./worker`, so a build script that only needs the SQL does not\n * pull in the router, and a Worker that only serves baselines does not pull in\n * React.\n */\n\nexport type {\n D1Like,\n D1PreparedLike,\n D1Value,\n R2Like,\n R2ObjectLike,\n TribunalBindings,\n} from './bindings.js';\nexport { base64Of, bytesOf } from './bindings.js';\nexport { INITIAL_VERSION, MIGRATIONS, SCHEMA, SCHEMA_VERSION, applySchema } from './schema.js';\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every shape this database has taken since it shipped, one entry per version.
|
|
3
|
+
*
|
|
4
|
+
* Apart from [`migrations.ts`](./migrations.js), which holds the frozen initial
|
|
5
|
+
* set, because the two are edited under opposite rules: that file must never
|
|
6
|
+
* change again, and this one only ever grows. Keeping them in one module meant
|
|
7
|
+
* every new table was appended a screen below a block carrying a comment saying
|
|
8
|
+
* it was frozen.
|
|
9
|
+
*
|
|
10
|
+
* The reading of both — which version they land on, and the batch that applies
|
|
11
|
+
* them — is in [`schema.ts`](./schema.js).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* One entry per version after `INITIAL_VERSION`, in order.
|
|
15
|
+
*
|
|
16
|
+
* A database that is already deployed cannot be given a new table by editing the
|
|
17
|
+
* statements that created it — `wrangler d1 migrations apply` tracks which files
|
|
18
|
+
* it has run, and a rewritten `0001` is a file it will never run again. So the
|
|
19
|
+
* initial set is frozen at the version it shipped at and every later shape is a
|
|
20
|
+
* step, generated into its own `.sql` beside it.
|
|
21
|
+
*
|
|
22
|
+
* Each step ends by writing its own version, so a database is never at a version
|
|
23
|
+
* whose shape it does not have — and a fresh database applying the initial set
|
|
24
|
+
* and every step in order arrives at exactly the same place as one that was
|
|
25
|
+
* deployed three versions ago.
|
|
26
|
+
*
|
|
27
|
+
* Additive only. A step that dropped or rewrote a column would be asking an
|
|
28
|
+
* append-only store to forget something, which is the one thing every trigger in
|
|
29
|
+
* this file exists to refuse.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MIGRATIONS: readonly (readonly string[])[];
|
|
32
|
+
//# sourceMappingURL=migration-steps.d.ts.map
|