@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,361 @@
|
|
|
1
|
+
import { identityFrom } from '@variance-authority/raster';
|
|
2
|
+
import { ReviewError, number, optionalNumber, optionalText, text, } from './review-rows.js';
|
|
3
|
+
/**
|
|
4
|
+
* A build, read back out of D1 and turned into what a reviewer is shown.
|
|
5
|
+
*
|
|
6
|
+
* Apart from [`review.ts`](./review.ts) because nothing here writes anything:
|
|
7
|
+
* given rows, these produce the summary, the current decision per subject, the
|
|
8
|
+
* docket and the per-subject view, and every one of them is safe to call twice.
|
|
9
|
+
*
|
|
10
|
+
* Two of the project's arguments are implemented here and nowhere else — that the
|
|
11
|
+
* *current* decision is the highest sequence rather than the newest timestamp,
|
|
12
|
+
* and that the docket is ranked by cause pixels rather than by area — and both
|
|
13
|
+
* are written on the function that carries them, because that is what a later
|
|
14
|
+
* edit will be looking at when it is about to undo one.
|
|
15
|
+
*/
|
|
16
|
+
export async function summarize(db, project, row) {
|
|
17
|
+
const build = text(row, 'build', 'a build');
|
|
18
|
+
const identity = identityFrom(JSON.parse(text(row, 'identity', 'a build')));
|
|
19
|
+
if (identity === null) {
|
|
20
|
+
throw new ReviewError(`build "${build}" carries an identity that is not a renderer identity`);
|
|
21
|
+
}
|
|
22
|
+
const counted = await db
|
|
23
|
+
.prepare('SELECT verdict, COUNT(*) AS n FROM build_subjects WHERE project = ? AND build = ? GROUP BY verdict')
|
|
24
|
+
.bind(project, build)
|
|
25
|
+
.all();
|
|
26
|
+
// Every verdict, including the ones nothing pends on. A map missing a key
|
|
27
|
+
// reports `undefined` where a build genuinely had none, and a review surface
|
|
28
|
+
// that cannot tell "no ignored subjects" from "this build predates ignores" is
|
|
29
|
+
// one an operator has to go to the database to trust.
|
|
30
|
+
const verdicts = {
|
|
31
|
+
unchanged: 0,
|
|
32
|
+
changed: 0,
|
|
33
|
+
new: 0,
|
|
34
|
+
incomparable: 0,
|
|
35
|
+
ignored: 0,
|
|
36
|
+
};
|
|
37
|
+
for (const entry of counted.results) {
|
|
38
|
+
const verdict = text(entry, 'verdict', 'a verdict count');
|
|
39
|
+
if (verdict in verdicts)
|
|
40
|
+
verdicts[verdict] = number(entry, 'n', 'a verdict count');
|
|
41
|
+
}
|
|
42
|
+
const decisions = await latestDecisions(db, project, build);
|
|
43
|
+
const needing = await db
|
|
44
|
+
.prepare(`SELECT subject FROM build_subjects
|
|
45
|
+
WHERE project = ? AND build = ? AND verdict IN ('changed', 'new', 'incomparable')`)
|
|
46
|
+
.bind(project, build)
|
|
47
|
+
.all();
|
|
48
|
+
const pending = needing.results.filter((subject) => !decisions.has(text(subject, 'subject', 'a build subject'))).length;
|
|
49
|
+
const skipped = await db
|
|
50
|
+
.prepare(`SELECT kind, COUNT(*) AS n FROM build_not_observed
|
|
51
|
+
WHERE project = ? AND build = ? GROUP BY kind`)
|
|
52
|
+
.bind(project, build)
|
|
53
|
+
.all();
|
|
54
|
+
const coverage = {
|
|
55
|
+
stated: number(row, 'says_not_observed', 'a build') !== 0,
|
|
56
|
+
failed: countOf(skipped.results, 'failed'),
|
|
57
|
+
excluded: countOf(skipped.results, 'excluded'),
|
|
58
|
+
unreached: countOf(skipped.results, 'unreached'),
|
|
59
|
+
};
|
|
60
|
+
const branch = optionalText(row, 'branch', 'a build');
|
|
61
|
+
const intent = optionalText(row, 'intent', 'a build');
|
|
62
|
+
return {
|
|
63
|
+
project,
|
|
64
|
+
build,
|
|
65
|
+
commit: text(row, 'commit', 'a build'),
|
|
66
|
+
at: text(row, 'at', 'a build'),
|
|
67
|
+
identity,
|
|
68
|
+
retention: text(row, 'retention', 'a build') === 'ephemeral' ? 'ephemeral' : 'durable',
|
|
69
|
+
verdicts,
|
|
70
|
+
decided: decisions.size,
|
|
71
|
+
pending,
|
|
72
|
+
coverage,
|
|
73
|
+
...(branch !== undefined ? { branch } : {}),
|
|
74
|
+
...(intent !== undefined ? { intent } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The current decision per subject, from an append-only table.
|
|
79
|
+
*
|
|
80
|
+
* `MAX(seq)` rather than `MAX(at_ms)`: two decisions can share a millisecond, and
|
|
81
|
+
* a tie broken arbitrarily would show a reviewer their earlier answer as the
|
|
82
|
+
* current one. The sequence is the order the rows were written and cannot tie.
|
|
83
|
+
*/
|
|
84
|
+
export async function latestDecisions(db, project, build) {
|
|
85
|
+
const rows = await db
|
|
86
|
+
.prepare(`SELECT d.subject, d.decision, d.decided_by, d.note, d.at
|
|
87
|
+
FROM decisions d
|
|
88
|
+
JOIN (SELECT subject, MAX(seq) AS seq FROM decisions
|
|
89
|
+
WHERE project = ? AND build = ? GROUP BY subject) latest
|
|
90
|
+
ON latest.seq = d.seq`)
|
|
91
|
+
.bind(project, build)
|
|
92
|
+
.all();
|
|
93
|
+
const decisions = new Map();
|
|
94
|
+
for (const row of rows.results) {
|
|
95
|
+
const note = optionalText(row, 'note', 'a decision');
|
|
96
|
+
decisions.set(text(row, 'subject', 'a decision'), {
|
|
97
|
+
decision: text(row, 'decision', 'a decision') === 'approved' ? 'approved' : 'rejected',
|
|
98
|
+
by: text(row, 'decided_by', 'a decision'),
|
|
99
|
+
at: text(row, 'at', 'a decision'),
|
|
100
|
+
...(note !== undefined ? { note } : {}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return decisions;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The docket: one entry per cause component, with collateral counted.
|
|
107
|
+
*
|
|
108
|
+
* Ranked by cause pixels rather than by total area, because area measures
|
|
109
|
+
* *displacement* — a container that only reflowed outranks the component that was
|
|
110
|
+
* edited, measured at 6× on one edit. Collateral is summed into the entry rather
|
|
111
|
+
* than listed, so one token change across 300 subjects is one review item with a
|
|
112
|
+
* count.
|
|
113
|
+
*/
|
|
114
|
+
export function docket(subjects) {
|
|
115
|
+
const causes = new Map();
|
|
116
|
+
let collateral = 0;
|
|
117
|
+
for (const subject of subjects) {
|
|
118
|
+
for (const region of subject.regions) {
|
|
119
|
+
if (!region.cause) {
|
|
120
|
+
collateral += region.pixels;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const name = region.component ?? '(unattributed)';
|
|
124
|
+
const entry = causes.get(name) ?? { subjects: new Set(), pixels: 0 };
|
|
125
|
+
entry.pixels += region.pixels;
|
|
126
|
+
entry.subjects.add(subject.subject);
|
|
127
|
+
if (entry.file === undefined && region.file !== undefined)
|
|
128
|
+
entry.file = region.file;
|
|
129
|
+
causes.set(name, entry);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return [...causes.entries()]
|
|
133
|
+
.map(([component, entry]) => ({
|
|
134
|
+
component,
|
|
135
|
+
subjects: [...entry.subjects].sort(),
|
|
136
|
+
pixels: entry.pixels,
|
|
137
|
+
// The same total on every entry, and deliberately so: collateral is a
|
|
138
|
+
// property of the build, not of one cause. Splitting it between causes
|
|
139
|
+
// would require deciding which edit pushed which box around, which is
|
|
140
|
+
// exactly the attribution the semantic tier declined to claim.
|
|
141
|
+
collateralPixels: collateral,
|
|
142
|
+
...(entry.file !== undefined ? { file: entry.file } : {}),
|
|
143
|
+
}))
|
|
144
|
+
.sort((left, right) => {
|
|
145
|
+
const byPixels = right.pixels - left.pixels;
|
|
146
|
+
if (byPixels !== 0)
|
|
147
|
+
return byPixels;
|
|
148
|
+
return left.component < right.component ? -1 : left.component > right.component ? 1 : 0;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
export function toSubjectView(row, decision) {
|
|
152
|
+
const what = 'a build subject';
|
|
153
|
+
const truncated = optionalText(row, 'truncated', what);
|
|
154
|
+
const missingFonts = optionalText(row, 'missing_fonts', what);
|
|
155
|
+
const findings = optionalText(row, 'findings', what);
|
|
156
|
+
const signals = optionalText(row, 'signals', what);
|
|
157
|
+
const ignored = optionalText(row, 'ignored', what);
|
|
158
|
+
const relaxed = optionalText(row, 'relaxed', what);
|
|
159
|
+
const moved = optionalText(row, 'moved', what);
|
|
160
|
+
const after = optionalText(row, 'after_key', what);
|
|
161
|
+
const width = row['candidate_width'];
|
|
162
|
+
const height = row['candidate_height'];
|
|
163
|
+
const wasWide = row['baseline_width'];
|
|
164
|
+
const wasTall = row['baseline_height'];
|
|
165
|
+
return {
|
|
166
|
+
subject: text(row, 'subject', what),
|
|
167
|
+
verdict: text(row, 'verdict', what),
|
|
168
|
+
because: text(row, 'because', what),
|
|
169
|
+
changedPixels: number(row, 'changed_pixels', what),
|
|
170
|
+
regions: JSON.parse(text(row, 'regions', what)),
|
|
171
|
+
has: {
|
|
172
|
+
before: optionalText(row, 'before_key', what) !== undefined,
|
|
173
|
+
after: after !== undefined,
|
|
174
|
+
diff: optionalText(row, 'diff_key', what) !== undefined,
|
|
175
|
+
},
|
|
176
|
+
approvable: after !== undefined && optionalText(row, 'candidate_document_digest', what) !== undefined,
|
|
177
|
+
decision,
|
|
178
|
+
...(truncated !== undefined
|
|
179
|
+
? { truncated: JSON.parse(truncated) }
|
|
180
|
+
: {}),
|
|
181
|
+
...(missingFonts !== undefined ? { missingFonts: JSON.parse(missingFonts) } : {}),
|
|
182
|
+
...(findings !== undefined ? { findings: JSON.parse(findings) } : {}),
|
|
183
|
+
...(signals !== undefined
|
|
184
|
+
? { signals: JSON.parse(signals) }
|
|
185
|
+
: {}),
|
|
186
|
+
// Absent stays absent. A row written before step 10, or by a run whose
|
|
187
|
+
// config declared nothing, says nothing here — and a `{}` invented in its
|
|
188
|
+
// place would tell a reader a rule absorbed nothing.
|
|
189
|
+
...(ignored !== undefined
|
|
190
|
+
? { ignored: JSON.parse(ignored) }
|
|
191
|
+
: {}),
|
|
192
|
+
...(relaxed !== undefined
|
|
193
|
+
? { relaxed: JSON.parse(relaxed) }
|
|
194
|
+
: {}),
|
|
195
|
+
...(moved !== undefined ? { moved: JSON.parse(moved) } : {}),
|
|
196
|
+
...(typeof width === 'number' && typeof height === 'number' ? { size: { width, height } } : {}),
|
|
197
|
+
...(typeof wasWide === 'number' && typeof wasTall === 'number'
|
|
198
|
+
? { baseline: { width: wasWide, height: wasTall } }
|
|
199
|
+
: {}),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* A stored variation, read back as the record the report wrote.
|
|
204
|
+
*
|
|
205
|
+
* Every optional field stays optional. `identical` is the one worth naming: the
|
|
206
|
+
* column is nullable and `NULL` is read as absent rather than as `false`, because
|
|
207
|
+
* a pair nothing compared and a pair compared and found to differ are different
|
|
208
|
+
* claims, and only the second is something a reviewer can act on.
|
|
209
|
+
*
|
|
210
|
+
* `how` is narrowed rather than cast. A row holding a word this deployment does
|
|
211
|
+
* not know is read as *unstated* — the sentence in `because` still says what
|
|
212
|
+
* happened, and asserting `declared` over an unrecognised value would put "a
|
|
213
|
+
* person said so" on an inference.
|
|
214
|
+
*/
|
|
215
|
+
export function toVariation(row) {
|
|
216
|
+
const what = 'a build variation';
|
|
217
|
+
const parent = optionalText(row, 'parent', what);
|
|
218
|
+
const identical = row['identical'];
|
|
219
|
+
const bands = optionalText(row, 'bands', what);
|
|
220
|
+
const unobserved = optionalText(row, 'unobserved', what);
|
|
221
|
+
const components = optionalText(row, 'components', what);
|
|
222
|
+
const digest = optionalText(row, 'digest', what);
|
|
223
|
+
const how = optionalText(row, 'how', what);
|
|
224
|
+
return {
|
|
225
|
+
subject: text(row, 'subject', what),
|
|
226
|
+
because: text(row, 'because', what),
|
|
227
|
+
...(parent !== undefined ? { parent } : {}),
|
|
228
|
+
...(identical === null || identical === undefined ? {} : { identical: Number(identical) !== 0 }),
|
|
229
|
+
...(bands !== undefined ? { bands: JSON.parse(bands) } : {}),
|
|
230
|
+
...(unobserved !== undefined ? { unobserved: JSON.parse(unobserved) } : {}),
|
|
231
|
+
...(components !== undefined ? { components: JSON.parse(components) } : {}),
|
|
232
|
+
...(digest !== undefined ? { digest } : {}),
|
|
233
|
+
...(how === 'declared' || how === 'named' ? { how } : {}),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The reach rows, read back as the section the page draws.
|
|
238
|
+
*
|
|
239
|
+
* The one rule worth stating: `whole` present means `subjects` stays *absent*,
|
|
240
|
+
* even though the query returned an empty list either way. A refusal rendered as
|
|
241
|
+
* an empty attribution reads as "this commit reaches none of your subjects",
|
|
242
|
+
* which is the sentence somebody merges on — and it is the opposite of what the
|
|
243
|
+
* row says.
|
|
244
|
+
*/
|
|
245
|
+
export function toReach(row, subjects) {
|
|
246
|
+
const what = 'a build reach';
|
|
247
|
+
const whole = optionalText(row, 'whole', what);
|
|
248
|
+
const unscanned = optionalText(row, 'unscanned', what);
|
|
249
|
+
const opaque = optionalText(row, 'opaque', what);
|
|
250
|
+
const attributed = {};
|
|
251
|
+
for (const entry of subjects) {
|
|
252
|
+
const because = 'a reached subject';
|
|
253
|
+
const through = optionalText(entry, 'through', because);
|
|
254
|
+
const trail = optionalText(entry, 'trail', because);
|
|
255
|
+
attributed[text(entry, 'subject', because)] = {
|
|
256
|
+
reached: Number(entry['reached']) !== 0,
|
|
257
|
+
through: through === undefined ? [] : JSON.parse(through),
|
|
258
|
+
because: text(entry, 'because', because),
|
|
259
|
+
...(trail === undefined ? {} : { trail: JSON.parse(trail) }),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
against: text(row, 'against_ref', what),
|
|
264
|
+
changed: JSON.parse(text(row, 'changed', what)),
|
|
265
|
+
components: JSON.parse(text(row, 'components', what)),
|
|
266
|
+
...(whole === undefined ? { subjects: attributed } : { whole }),
|
|
267
|
+
...(unscanned === undefined ? {} : { unscanned: JSON.parse(unscanned) }),
|
|
268
|
+
...(opaque === undefined ? {} : { opaque: JSON.parse(opaque) }),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* One census row, read back as an edge a sentence can be built from.
|
|
273
|
+
*
|
|
274
|
+
* No shape checking beyond the parse, for the reason `toReach` gives: the
|
|
275
|
+
* columns were written from a typed value by `ingest`.
|
|
276
|
+
*/
|
|
277
|
+
export function toPlacement(row) {
|
|
278
|
+
const what = 'a composition entry';
|
|
279
|
+
return {
|
|
280
|
+
component: text(row, 'component', what),
|
|
281
|
+
subjects: JSON.parse(text(row, 'subjects', what)),
|
|
282
|
+
within: JSON.parse(text(row, 'within', what)),
|
|
283
|
+
createdBy: JSON.parse(text(row, 'created_by', what)),
|
|
284
|
+
renders: JSON.parse(text(row, 'renders', what)),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* One attributed movement, read back as the run wrote it.
|
|
289
|
+
*
|
|
290
|
+
* `cause` is narrowed by comparison against the five rungs rather than cast. It
|
|
291
|
+
* is the only column here a later schema could widen, and a row carrying a rung
|
|
292
|
+
* this build does not know is a row it must refuse rather than print.
|
|
293
|
+
*/
|
|
294
|
+
export function toMovement(row) {
|
|
295
|
+
const what = 'a movement';
|
|
296
|
+
const cause = text(row, 'cause', what);
|
|
297
|
+
if (!RUNGS.includes(cause)) {
|
|
298
|
+
throw new ReviewError(`${what} carries a cause this build does not know: "${cause}"`);
|
|
299
|
+
}
|
|
300
|
+
const file = optionalText(row, 'file', what);
|
|
301
|
+
const tokens = optionalText(row, 'tokens', what);
|
|
302
|
+
const upstream = optionalText(row, 'upstream', what);
|
|
303
|
+
const through = optionalText(row, 'through', what);
|
|
304
|
+
const standing = optionalText(row, 'standing', what);
|
|
305
|
+
const compared = optionalNumber(row, 'compared', what);
|
|
306
|
+
return {
|
|
307
|
+
subject: text(row, 'subject', what),
|
|
308
|
+
component: text(row, 'component', what),
|
|
309
|
+
cause: cause,
|
|
310
|
+
because: text(row, 'because', what),
|
|
311
|
+
bands: JSON.parse(text(row, 'bands', what)),
|
|
312
|
+
held: JSON.parse(text(row, 'held', what)),
|
|
313
|
+
...(compared === undefined ? {} : { compared }),
|
|
314
|
+
...(file === undefined ? {} : { file }),
|
|
315
|
+
...(tokens === undefined ? {} : { tokens: JSON.parse(tokens) }),
|
|
316
|
+
...(upstream === undefined ? {} : { upstream }),
|
|
317
|
+
...(through === undefined ? {} : { through: JSON.parse(through) }),
|
|
318
|
+
...(standing === undefined ? {} : { standing: standing === 'flake' ? 'flake' : 'suspect' }),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const RUNGS = [
|
|
322
|
+
'edited',
|
|
323
|
+
'token',
|
|
324
|
+
'upstream',
|
|
325
|
+
'contradicted',
|
|
326
|
+
'unexplained',
|
|
327
|
+
];
|
|
328
|
+
// Named, not defaulted-to-`failed`. The stored word is the run's conclusion, and
|
|
329
|
+
// anything the reader does not recognise is a hole in this reader — which is
|
|
330
|
+
// what `failed` says — rather than in the run.
|
|
331
|
+
const NOT_OBSERVED = ['excluded', 'unreached', 'failed'];
|
|
332
|
+
export function toNotObserved(row) {
|
|
333
|
+
const what = 'a not-observed entry';
|
|
334
|
+
const kind = text(row, 'kind', what);
|
|
335
|
+
return {
|
|
336
|
+
subject: text(row, 'subject', what),
|
|
337
|
+
kind: NOT_OBSERVED.find((known) => known === kind) ?? 'failed',
|
|
338
|
+
because: text(row, 'because', what),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The declaration ledgers, read back whole or read back as absent.
|
|
343
|
+
*
|
|
344
|
+
* No shape checking beyond the parse, and that is the same trade `toReach` makes:
|
|
345
|
+
* the column was written from a typed value by `ingest`, and a reader that
|
|
346
|
+
* re-validated it would be a second opinion about a shape this repository owns.
|
|
347
|
+
* What it does refuse is turning a missing column into an empty ledger.
|
|
348
|
+
*/
|
|
349
|
+
export function toDeclarations(row) {
|
|
350
|
+
const ignores = optionalText(row, 'ignores', 'a build');
|
|
351
|
+
const sensitivities = optionalText(row, 'sensitivities', 'a build');
|
|
352
|
+
return {
|
|
353
|
+
ignores: ignores === undefined ? null : JSON.parse(ignores),
|
|
354
|
+
sensitivities: sensitivities === undefined ? null : JSON.parse(sensitivities),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function countOf(rows, kind) {
|
|
358
|
+
const found = rows.find((row) => row['kind'] === kind);
|
|
359
|
+
return found === undefined ? 0 : number(found, 'n', 'a coverage count');
|
|
360
|
+
}
|
|
361
|
+
//# sourceMappingURL=review-read.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-read.js","sourceRoot":"","sources":["../src/review-read.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAc1D,OAAO,EACL,WAAW,EACX,MAAM,EACN,cAAc,EACd,YAAY,EACZ,IAAI,GAEL,MAAM,kBAAkB,CAAC;AAa1B;;;;;;;;;;;;GAYG;AAEH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,EAAU,EAAE,OAAe,EAAE,GAAQ;IACnE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IAC5C,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,SAAS,CAAC,CAAY,CAAC,CAAC;IACvF,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,MAAM,IAAI,WAAW,CAAC,UAAU,KAAK,uDAAuD,CAAC,CAAC;IAChG,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,EAAE;SACrB,OAAO,CACN,oGAAoG,CACrG;SACA,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC;SACpB,GAAG,EAAO,CAAC;IAEd,0EAA0E;IAC1E,6EAA6E;IAC7E,+EAA+E;IAC/E,sDAAsD;IACtD,MAAM,QAAQ,GAAiD;QAC7D,SAAS,EAAE,CAAC;QACZ,OAAO,EAAE,CAAC;QACV,GAAG,EAAE,CAAC;QACN,YAAY,EAAE,CAAC;QACf,OAAO,EAAE,CAAC;KACX,CAAC;IACF,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,iBAAiB,CAAiC,CAAC;QAC1F,IAAI,OAAO,IAAI,QAAQ;YAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,iBAAiB,CAAC,CAAC;IACrF,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,eAAe,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,MAAM,EAAE;SACrB,OAAO,CACN;0FACoF,CACrF;SACA,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC;SACpB,GAAG,EAAO,CAAC;IAEd,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CACpC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,iBAAiB,CAAC,CAAC,CACzE,CAAC,MAAM,CAAC;IAET,MAAM,OAAO,GAAG,MAAM,EAAE;SACrB,OAAO,CACN;sDACgD,CACjD;SACA,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC;SACpB,GAAG,EAAO,CAAC;IAEd,MAAM,QAAQ,GAAa;QACzB,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,mBAAmB,EAAE,SAAS,CAAC,KAAK,CAAC;QACzD,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC;QAC1C,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC;QAC9C,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC;KACjD,CAAC;IAEF,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;IAEtD,OAAO;QACL,OAAO;QACP,KAAK;QACL,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,SAAS,CAAC;QACtC,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC;QAC9B,QAAQ;QACR,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,SAAS,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;QACtF,QAAQ;QACR,OAAO,EAAE,SAAS,CAAC,IAAI;QACvB,OAAO;QACP,QAAQ;QACR,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5C,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,EAAU,EACV,OAAe,EACf,KAAa;IAEb,MAAM,IAAI,GAAG,MAAM,EAAE;SAClB,OAAO,CACN;;;;iCAI2B,CAC5B;SACA,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC;SACpB,GAAG,EAAO,CAAC;IAEd,MAAM,SAAS,GAAG,IAAI,GAAG,EAA0B,CAAC;IACpD,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC;QACrD,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,EAAE;YAChD,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,YAAY,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU;YACtF,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,YAAY,EAAE,YAAY,CAAC;YACzC,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,YAAY,CAAC;YACjC,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,MAAM,CAAC,QAAgC;IACrD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoE,CAAC;IAC3F,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;YACrC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;gBAClB,UAAU,IAAI,MAAM,CAAC,MAAM,CAAC;gBAC5B,SAAS;YACX,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,IAAI,gBAAgB,CAAC;YAClD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAG,EAAU,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;YAC7E,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC;YAC9B,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACpC,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;gBAAE,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;YACpF,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;SACzB,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5B,SAAS;QACT,QAAQ,EAAE,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE;QACpC,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,sEAAsE;QACtE,uEAAuE;QACvE,sEAAsE;QACtE,+DAA+D;QAC/D,gBAAgB,EAAE,UAAU;QAC5B,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1D,CAAC,CAAC;SACF,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACpB,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC5C,IAAI,QAAQ,KAAK,CAAC;YAAE,OAAO,QAAQ,CAAC;QACpC,OAAO,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;AACP,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,GAAQ,EAAE,QAA+B;IACrE,MAAM,IAAI,GAAG,iBAAiB,CAAC;IAC/B,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;IACvD,MAAM,YAAY,GAAG,YAAY,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;IACrD,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACvC,MAAM,OAAO,GAAG,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAEvC,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC;QACnC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAiC;QACnE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC;QACnC,aAAa,EAAE,MAAM,CAAC,GAAG,EAAE,gBAAgB,EAAE,IAAI,CAAC;QAClD,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,CAAmB;QACjE,GAAG,EAAE;YACH,MAAM,EAAE,YAAY,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,KAAK,SAAS;YAC3D,KAAK,EAAE,KAAK,KAAK,SAAS;YAC1B,IAAI,EAAE,YAAY,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,KAAK,SAAS;SACxD;QACD,UAAU,EACR,KAAK,KAAK,SAAS,IAAI,YAAY,CAAC,GAAG,EAAE,2BAA2B,EAAE,IAAI,CAAC,KAAK,SAAS;QAC3F,QAAQ;QACR,GAAG,CAAC,SAAS,KAAK,SAAS;YACzB,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAwC,EAAE;YAC7E,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7F,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAoB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxF,GAAG,CAAC,OAAO,KAAK,SAAS;YACvB,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAiC,EAAE;YAClE,CAAC,CAAC,EAAE,CAAC;QACP,uEAAuE;QACvE,0EAA0E;QAC1E,qDAAqD;QACrD,GAAG,CAAC,OAAO,KAAK,SAAS;YACvB,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAiC,EAAE;YAClE,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,OAAO,KAAK,SAAS;YACvB,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAiC,EAAE;YAClE,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAA+B,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1F,GAAG,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/F,GAAG,CAAC,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,OAAO,KAAK,QAAQ;YAC5D,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;YACnD,CAAC,CAAC,EAAE,CAAC;KACR,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CAAC,GAAQ;IAClC,MAAM,IAAI,GAAG,mBAAmB,CAAC;IACjC,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IACjD,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAG,YAAY,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;IACzD,MAAM,UAAU,GAAG,YAAY,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAE3C,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC;QACnC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC;QACnC,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,GAAG,CAAC,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QAChG,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxE,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvF,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvF,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,GAAG,CAAC,GAAG,KAAK,UAAU,IAAI,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1D,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,OAAO,CAAC,GAAQ,EAAE,QAAwB;IACxD,MAAM,IAAI,GAAG,eAAe,CAAC;IAC7B,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IAEjD,MAAM,UAAU,GAAiC,EAAE,CAAC;IACpD,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,mBAAmB,CAAC;QACpC,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QACxD,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QACpD,UAAU,CAAC,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,GAAG;YAC5C,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC;YACvC,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAc;YACvE,OAAO,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC;YACxC,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAa,EAAE,CAAC;SACzE,CAAC;IACJ,CAAC;IAED,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,CAAC;QACvC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,CAAa;QAC3D,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,CAAuB;QAC3E,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;QAC/D,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAa,EAAE,CAAC;QACpF,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAgB,EAAE,CAAC;KAC/E,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAQ;IAClC,MAAM,IAAI,GAAG,qBAAqB,CAAC;IACnC,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC;QACvC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,CAAa;QAC7D,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAa;QACzD,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,CAAa;QAChE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,CAAa;KAC5D,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,GAAQ;IACjC,MAAM,IAAI,GAAG,YAAY,CAAC;IAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAA8B,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,+CAA+C,KAAK,GAAG,CAAC,CAAC;IACxF,CAAC;IAED,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IACjD,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;IACrD,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;IAEvD,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC;QACnC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC;QACvC,KAAK,EAAE,KAA8B;QACrC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC;QACnC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAa;QACvD,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAa;QACrD,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;QAC/C,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;QACvC,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAa,EAAE,CAAC;QAC3E,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;QAC/C,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAa,EAAE,CAAC;QAC9E,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;KAC5F,CAAC;AACJ,CAAC;AAED,MAAM,KAAK,GAAqC;IAC9C,QAAQ;IACR,OAAO;IACP,UAAU;IACV,cAAc;IACd,aAAa;CACd,CAAC;AAEF,iFAAiF;AACjF,6EAA6E;AAC7E,+CAA+C;AAC/C,MAAM,YAAY,GAAmC,CAAC,UAAU,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;AAEzF,MAAM,UAAU,aAAa,CAAC,GAAQ;IACpC,MAAM,IAAI,GAAG,sBAAsB,CAAC;IACpC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IACrC,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC;QACnC,IAAI,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,QAAQ;QAC9D,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC;KACpC,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,GAAQ;IACrC,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;IACxD,MAAM,aAAa,GAAG,YAAY,CAAC,GAAG,EAAE,eAAe,EAAE,SAAS,CAAC,CAAC;IACpE,OAAO;QACL,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAkB;QAC7E,aAAa,EACX,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,aAAa,CAAuB;KACxF,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,IAAoB,EAAE,IAAY;IACjD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC;IACvD,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,kBAAkB,CAAC,CAAC;AAC1E,CAAC","sourcesContent":["import { identityFrom } from '@variance-authority/raster';\nimport type {\n FindingRecord,\n IgnoreLedger,\n NotObserved,\n ObservationRecord,\n ReachHole,\n ReachedComponent,\n RegionRecord,\n SensitivityLedger,\n SubjectReach,\n VariationRecord,\n} from '@variance-authority/report';\nimport type { D1Like } from './bindings.js';\nimport {\n ReviewError,\n number,\n optionalNumber,\n optionalText,\n text,\n type Row,\n} from './review-rows.js';\nimport type {\n BuildSummary,\n Cause,\n Coverage,\n Declarations,\n DecisionRecord,\n MovementView,\n Placement,\n ReachView,\n SubjectView,\n} from './review-types.js';\n\n/**\n * A build, read back out of D1 and turned into what a reviewer is shown.\n *\n * Apart from [`review.ts`](./review.ts) because nothing here writes anything:\n * given rows, these produce the summary, the current decision per subject, the\n * docket and the per-subject view, and every one of them is safe to call twice.\n *\n * Two of the project's arguments are implemented here and nowhere else — that the\n * *current* decision is the highest sequence rather than the newest timestamp,\n * and that the docket is ranked by cause pixels rather than by area — and both\n * are written on the function that carries them, because that is what a later\n * edit will be looking at when it is about to undo one.\n */\n\nexport async function summarize(db: D1Like, project: string, row: Row): Promise<BuildSummary> {\n const build = text(row, 'build', 'a build');\n const identity = identityFrom(JSON.parse(text(row, 'identity', 'a build')) as unknown);\n if (identity === null) {\n throw new ReviewError(`build \"${build}\" carries an identity that is not a renderer identity`);\n }\n\n const counted = await db\n .prepare(\n 'SELECT verdict, COUNT(*) AS n FROM build_subjects WHERE project = ? AND build = ? GROUP BY verdict',\n )\n .bind(project, build)\n .all<Row>();\n\n // Every verdict, including the ones nothing pends on. A map missing a key\n // reports `undefined` where a build genuinely had none, and a review surface\n // that cannot tell \"no ignored subjects\" from \"this build predates ignores\" is\n // one an operator has to go to the database to trust.\n const verdicts: Record<ObservationRecord['verdict'], number> = {\n unchanged: 0,\n changed: 0,\n new: 0,\n incomparable: 0,\n ignored: 0,\n };\n for (const entry of counted.results) {\n const verdict = text(entry, 'verdict', 'a verdict count') as ObservationRecord['verdict'];\n if (verdict in verdicts) verdicts[verdict] = number(entry, 'n', 'a verdict count');\n }\n\n const decisions = await latestDecisions(db, project, build);\n const needing = await db\n .prepare(\n `SELECT subject FROM build_subjects\n WHERE project = ? AND build = ? AND verdict IN ('changed', 'new', 'incomparable')`,\n )\n .bind(project, build)\n .all<Row>();\n\n const pending = needing.results.filter(\n (subject) => !decisions.has(text(subject, 'subject', 'a build subject')),\n ).length;\n\n const skipped = await db\n .prepare(\n `SELECT kind, COUNT(*) AS n FROM build_not_observed\n WHERE project = ? AND build = ? GROUP BY kind`,\n )\n .bind(project, build)\n .all<Row>();\n\n const coverage: Coverage = {\n stated: number(row, 'says_not_observed', 'a build') !== 0,\n failed: countOf(skipped.results, 'failed'),\n excluded: countOf(skipped.results, 'excluded'),\n unreached: countOf(skipped.results, 'unreached'),\n };\n\n const branch = optionalText(row, 'branch', 'a build');\n const intent = optionalText(row, 'intent', 'a build');\n\n return {\n project,\n build,\n commit: text(row, 'commit', 'a build'),\n at: text(row, 'at', 'a build'),\n identity,\n retention: text(row, 'retention', 'a build') === 'ephemeral' ? 'ephemeral' : 'durable',\n verdicts,\n decided: decisions.size,\n pending,\n coverage,\n ...(branch !== undefined ? { branch } : {}),\n ...(intent !== undefined ? { intent } : {}),\n };\n}\n\n/**\n * The current decision per subject, from an append-only table.\n *\n * `MAX(seq)` rather than `MAX(at_ms)`: two decisions can share a millisecond, and\n * a tie broken arbitrarily would show a reviewer their earlier answer as the\n * current one. The sequence is the order the rows were written and cannot tie.\n */\nexport async function latestDecisions(\n db: D1Like,\n project: string,\n build: string,\n): Promise<ReadonlyMap<string, DecisionRecord>> {\n const rows = await db\n .prepare(\n `SELECT d.subject, d.decision, d.decided_by, d.note, d.at\n FROM decisions d\n JOIN (SELECT subject, MAX(seq) AS seq FROM decisions\n WHERE project = ? AND build = ? GROUP BY subject) latest\n ON latest.seq = d.seq`,\n )\n .bind(project, build)\n .all<Row>();\n\n const decisions = new Map<string, DecisionRecord>();\n for (const row of rows.results) {\n const note = optionalText(row, 'note', 'a decision');\n decisions.set(text(row, 'subject', 'a decision'), {\n decision: text(row, 'decision', 'a decision') === 'approved' ? 'approved' : 'rejected',\n by: text(row, 'decided_by', 'a decision'),\n at: text(row, 'at', 'a decision'),\n ...(note !== undefined ? { note } : {}),\n });\n }\n return decisions;\n}\n\n/**\n * The docket: one entry per cause component, with collateral counted.\n *\n * Ranked by cause pixels rather than by total area, because area measures\n * *displacement* — a container that only reflowed outranks the component that was\n * edited, measured at 6× on one edit. Collateral is summed into the entry rather\n * than listed, so one token change across 300 subjects is one review item with a\n * count.\n */\nexport function docket(subjects: readonly SubjectView[]): readonly Cause[] {\n const causes = new Map<string, { file?: string; subjects: Set<string>; pixels: number }>();\n let collateral = 0;\n\n for (const subject of subjects) {\n for (const region of subject.regions) {\n if (!region.cause) {\n collateral += region.pixels;\n continue;\n }\n const name = region.component ?? '(unattributed)';\n const entry = causes.get(name) ?? { subjects: new Set<string>(), pixels: 0 };\n entry.pixels += region.pixels;\n entry.subjects.add(subject.subject);\n if (entry.file === undefined && region.file !== undefined) entry.file = region.file;\n causes.set(name, entry);\n }\n }\n\n return [...causes.entries()]\n .map(([component, entry]) => ({\n component,\n subjects: [...entry.subjects].sort(),\n pixels: entry.pixels,\n // The same total on every entry, and deliberately so: collateral is a\n // property of the build, not of one cause. Splitting it between causes\n // would require deciding which edit pushed which box around, which is\n // exactly the attribution the semantic tier declined to claim.\n collateralPixels: collateral,\n ...(entry.file !== undefined ? { file: entry.file } : {}),\n }))\n .sort((left, right) => {\n const byPixels = right.pixels - left.pixels;\n if (byPixels !== 0) return byPixels;\n return left.component < right.component ? -1 : left.component > right.component ? 1 : 0;\n });\n}\n\nexport function toSubjectView(row: Row, decision: DecisionRecord | null): SubjectView {\n const what = 'a build subject';\n const truncated = optionalText(row, 'truncated', what);\n const missingFonts = optionalText(row, 'missing_fonts', what);\n const findings = optionalText(row, 'findings', what);\n const signals = optionalText(row, 'signals', what);\n const ignored = optionalText(row, 'ignored', what);\n const relaxed = optionalText(row, 'relaxed', what);\n const moved = optionalText(row, 'moved', what);\n const after = optionalText(row, 'after_key', what);\n const width = row['candidate_width'];\n const height = row['candidate_height'];\n const wasWide = row['baseline_width'];\n const wasTall = row['baseline_height'];\n\n return {\n subject: text(row, 'subject', what),\n verdict: text(row, 'verdict', what) as ObservationRecord['verdict'],\n because: text(row, 'because', what),\n changedPixels: number(row, 'changed_pixels', what),\n regions: JSON.parse(text(row, 'regions', what)) as RegionRecord[],\n has: {\n before: optionalText(row, 'before_key', what) !== undefined,\n after: after !== undefined,\n diff: optionalText(row, 'diff_key', what) !== undefined,\n },\n approvable:\n after !== undefined && optionalText(row, 'candidate_document_digest', what) !== undefined,\n decision,\n ...(truncated !== undefined\n ? { truncated: JSON.parse(truncated) as { regions: number; pixels: number } }\n : {}),\n ...(missingFonts !== undefined ? { missingFonts: JSON.parse(missingFonts) as string[] } : {}),\n ...(findings !== undefined ? { findings: JSON.parse(findings) as FindingRecord[] } : {}),\n ...(signals !== undefined\n ? { signals: JSON.parse(signals) as ObservationRecord['signals'] }\n : {}),\n // Absent stays absent. A row written before step 10, or by a run whose\n // config declared nothing, says nothing here — and a `{}` invented in its\n // place would tell a reader a rule absorbed nothing.\n ...(ignored !== undefined\n ? { ignored: JSON.parse(ignored) as ObservationRecord['ignored'] }\n : {}),\n ...(relaxed !== undefined\n ? { relaxed: JSON.parse(relaxed) as ObservationRecord['relaxed'] }\n : {}),\n ...(moved !== undefined ? { moved: JSON.parse(moved) as ObservationRecord['moved'] } : {}),\n ...(typeof width === 'number' && typeof height === 'number' ? { size: { width, height } } : {}),\n ...(typeof wasWide === 'number' && typeof wasTall === 'number'\n ? { baseline: { width: wasWide, height: wasTall } }\n : {}),\n };\n}\n\n/**\n * A stored variation, read back as the record the report wrote.\n *\n * Every optional field stays optional. `identical` is the one worth naming: the\n * column is nullable and `NULL` is read as absent rather than as `false`, because\n * a pair nothing compared and a pair compared and found to differ are different\n * claims, and only the second is something a reviewer can act on.\n *\n * `how` is narrowed rather than cast. A row holding a word this deployment does\n * not know is read as *unstated* — the sentence in `because` still says what\n * happened, and asserting `declared` over an unrecognised value would put \"a\n * person said so\" on an inference.\n */\nexport function toVariation(row: Row): VariationRecord {\n const what = 'a build variation';\n const parent = optionalText(row, 'parent', what);\n const identical = row['identical'];\n const bands = optionalText(row, 'bands', what);\n const unobserved = optionalText(row, 'unobserved', what);\n const components = optionalText(row, 'components', what);\n const digest = optionalText(row, 'digest', what);\n const how = optionalText(row, 'how', what);\n\n return {\n subject: text(row, 'subject', what),\n because: text(row, 'because', what),\n ...(parent !== undefined ? { parent } : {}),\n ...(identical === null || identical === undefined ? {} : { identical: Number(identical) !== 0 }),\n ...(bands !== undefined ? { bands: JSON.parse(bands) as string[] } : {}),\n ...(unobserved !== undefined ? { unobserved: JSON.parse(unobserved) as string[] } : {}),\n ...(components !== undefined ? { components: JSON.parse(components) as string[] } : {}),\n ...(digest !== undefined ? { digest } : {}),\n ...(how === 'declared' || how === 'named' ? { how } : {}),\n };\n}\n\n/**\n * The reach rows, read back as the section the page draws.\n *\n * The one rule worth stating: `whole` present means `subjects` stays *absent*,\n * even though the query returned an empty list either way. A refusal rendered as\n * an empty attribution reads as \"this commit reaches none of your subjects\",\n * which is the sentence somebody merges on — and it is the opposite of what the\n * row says.\n */\nexport function toReach(row: Row, subjects: readonly Row[]): ReachView {\n const what = 'a build reach';\n const whole = optionalText(row, 'whole', what);\n const unscanned = optionalText(row, 'unscanned', what);\n const opaque = optionalText(row, 'opaque', what);\n\n const attributed: Record<string, SubjectReach> = {};\n for (const entry of subjects) {\n const because = 'a reached subject';\n const through = optionalText(entry, 'through', because);\n const trail = optionalText(entry, 'trail', because);\n attributed[text(entry, 'subject', because)] = {\n reached: Number(entry['reached']) !== 0,\n through: through === undefined ? [] : (JSON.parse(through) as string[]),\n because: text(entry, 'because', because),\n ...(trail === undefined ? {} : { trail: JSON.parse(trail) as string[] }),\n };\n }\n\n return {\n against: text(row, 'against_ref', what),\n changed: JSON.parse(text(row, 'changed', what)) as string[],\n components: JSON.parse(text(row, 'components', what)) as ReachedComponent[],\n ...(whole === undefined ? { subjects: attributed } : { whole }),\n ...(unscanned === undefined ? {} : { unscanned: JSON.parse(unscanned) as string[] }),\n ...(opaque === undefined ? {} : { opaque: JSON.parse(opaque) as ReachHole[] }),\n };\n}\n\n/**\n * One census row, read back as an edge a sentence can be built from.\n *\n * No shape checking beyond the parse, for the reason `toReach` gives: the\n * columns were written from a typed value by `ingest`.\n */\nexport function toPlacement(row: Row): Placement {\n const what = 'a composition entry';\n return {\n component: text(row, 'component', what),\n subjects: JSON.parse(text(row, 'subjects', what)) as string[],\n within: JSON.parse(text(row, 'within', what)) as string[],\n createdBy: JSON.parse(text(row, 'created_by', what)) as string[],\n renders: JSON.parse(text(row, 'renders', what)) as string[],\n };\n}\n\n/**\n * One attributed movement, read back as the run wrote it.\n *\n * `cause` is narrowed by comparison against the five rungs rather than cast. It\n * is the only column here a later schema could widen, and a row carrying a rung\n * this build does not know is a row it must refuse rather than print.\n */\nexport function toMovement(row: Row): MovementView {\n const what = 'a movement';\n const cause = text(row, 'cause', what);\n if (!RUNGS.includes(cause as MovementView['cause'])) {\n throw new ReviewError(`${what} carries a cause this build does not know: \"${cause}\"`);\n }\n\n const file = optionalText(row, 'file', what);\n const tokens = optionalText(row, 'tokens', what);\n const upstream = optionalText(row, 'upstream', what);\n const through = optionalText(row, 'through', what);\n const standing = optionalText(row, 'standing', what);\n const compared = optionalNumber(row, 'compared', what);\n\n return {\n subject: text(row, 'subject', what),\n component: text(row, 'component', what),\n cause: cause as MovementView['cause'],\n because: text(row, 'because', what),\n bands: JSON.parse(text(row, 'bands', what)) as string[],\n held: JSON.parse(text(row, 'held', what)) as string[],\n ...(compared === undefined ? {} : { compared }),\n ...(file === undefined ? {} : { file }),\n ...(tokens === undefined ? {} : { tokens: JSON.parse(tokens) as string[] }),\n ...(upstream === undefined ? {} : { upstream }),\n ...(through === undefined ? {} : { through: JSON.parse(through) as string[] }),\n ...(standing === undefined ? {} : { standing: standing === 'flake' ? 'flake' : 'suspect' }),\n };\n}\n\nconst RUNGS: readonly MovementView['cause'][] = [\n 'edited',\n 'token',\n 'upstream',\n 'contradicted',\n 'unexplained',\n];\n\n// Named, not defaulted-to-`failed`. The stored word is the run's conclusion, and\n// anything the reader does not recognise is a hole in this reader — which is\n// what `failed` says — rather than in the run.\nconst NOT_OBSERVED: readonly NotObserved['kind'][] = ['excluded', 'unreached', 'failed'];\n\nexport function toNotObserved(row: Row): NotObserved {\n const what = 'a not-observed entry';\n const kind = text(row, 'kind', what);\n return {\n subject: text(row, 'subject', what),\n kind: NOT_OBSERVED.find((known) => known === kind) ?? 'failed',\n because: text(row, 'because', what),\n };\n}\n\n/**\n * The declaration ledgers, read back whole or read back as absent.\n *\n * No shape checking beyond the parse, and that is the same trade `toReach` makes:\n * the column was written from a typed value by `ingest`, and a reader that\n * re-validated it would be a second opinion about a shape this repository owns.\n * What it does refuse is turning a missing column into an empty ledger.\n */\nexport function toDeclarations(row: Row): Declarations {\n const ignores = optionalText(row, 'ignores', 'a build');\n const sensitivities = optionalText(row, 'sensitivities', 'a build');\n return {\n ignores: ignores === undefined ? null : (JSON.parse(ignores) as IgnoreLedger),\n sensitivities:\n sensitivities === undefined ? null : (JSON.parse(sensitivities) as SensitivityLedger),\n };\n}\n\nfunction countOf(rows: readonly Row[], kind: string): number {\n const found = rows.find((row) => row['kind'] === kind);\n return found === undefined ? 0 : number(found, 'n', 'a coverage count');\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a review operation says no, and every value that can make it say so.
|
|
3
|
+
*
|
|
4
|
+
* `ReviewError` is a leaf on purpose: the store, the reader and the writer all
|
|
5
|
+
* throw it, and a class everything imports cannot live in a file that imports
|
|
6
|
+
* them back. The readers sit with it because they are where most of these
|
|
7
|
+
* refusals come from — a stored row is validated rather than cast, for the same
|
|
8
|
+
* reason [`history-rows.ts`](./history-rows.ts) does it — and `instant` is that
|
|
9
|
+
* refusal applied to a timestamp on the way in rather than to a column on the way
|
|
10
|
+
* out.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* A review operation that cannot be carried out, as distinct from a platform
|
|
14
|
+
* that could not be reached.
|
|
15
|
+
*
|
|
16
|
+
* Separate from `RasterStoreError` because they route differently: this is a 4xx
|
|
17
|
+
* — the caller asked for something that is not so — and a store failure is a 5xx
|
|
18
|
+
* and a stopped run. A single error type would make an operator's dashboard show
|
|
19
|
+
* "Cloudflare is down" for a reviewer who clicked approve on the wrong subject.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ReviewError extends Error {
|
|
22
|
+
readonly name = "ReviewError";
|
|
23
|
+
}
|
|
24
|
+
export type Row = Record<string, unknown>;
|
|
25
|
+
export declare function text(row: Row, column: string, what: string): string;
|
|
26
|
+
export declare function optionalText(row: Row, column: string, what: string): string | undefined;
|
|
27
|
+
export declare function optionalNumber(row: Row, column: string, what: string): number | undefined;
|
|
28
|
+
export declare function number(row: Row, column: string, what: string): number;
|
|
29
|
+
export declare function instant(at: string, what: string): number;
|
|
30
|
+
//# sourceMappingURL=review-rows.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a review operation says no, and every value that can make it say so.
|
|
3
|
+
*
|
|
4
|
+
* `ReviewError` is a leaf on purpose: the store, the reader and the writer all
|
|
5
|
+
* throw it, and a class everything imports cannot live in a file that imports
|
|
6
|
+
* them back. The readers sit with it because they are where most of these
|
|
7
|
+
* refusals come from — a stored row is validated rather than cast, for the same
|
|
8
|
+
* reason [`history-rows.ts`](./history-rows.ts) does it — and `instant` is that
|
|
9
|
+
* refusal applied to a timestamp on the way in rather than to a column on the way
|
|
10
|
+
* out.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* A review operation that cannot be carried out, as distinct from a platform
|
|
14
|
+
* that could not be reached.
|
|
15
|
+
*
|
|
16
|
+
* Separate from `RasterStoreError` because they route differently: this is a 4xx
|
|
17
|
+
* — the caller asked for something that is not so — and a store failure is a 5xx
|
|
18
|
+
* and a stopped run. A single error type would make an operator's dashboard show
|
|
19
|
+
* "Cloudflare is down" for a reviewer who clicked approve on the wrong subject.
|
|
20
|
+
*/
|
|
21
|
+
export class ReviewError extends Error {
|
|
22
|
+
name = 'ReviewError';
|
|
23
|
+
}
|
|
24
|
+
export function text(row, column, what) {
|
|
25
|
+
const value = row[column];
|
|
26
|
+
if (typeof value !== 'string') {
|
|
27
|
+
throw new ReviewError(`${what} has a \`${column}\` that is not text`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
export function optionalText(row, column, what) {
|
|
32
|
+
const value = row[column];
|
|
33
|
+
if (value === null || value === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (typeof value !== 'string') {
|
|
36
|
+
throw new ReviewError(`${what} has a \`${column}\` that is neither text nor null`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
export function optionalNumber(row, column, what) {
|
|
41
|
+
const value = row[column];
|
|
42
|
+
if (value === null || value === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (typeof value === 'bigint')
|
|
45
|
+
return Number(value);
|
|
46
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
47
|
+
throw new ReviewError(`${what} has a \`${column}\` that is neither a number nor null`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
export function number(row, column, what) {
|
|
52
|
+
const value = row[column];
|
|
53
|
+
if (typeof value === 'bigint')
|
|
54
|
+
return Number(value);
|
|
55
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
56
|
+
throw new ReviewError(`${what} has a \`${column}\` that is not a number`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
export function instant(at, what) {
|
|
61
|
+
const parsed = Date.parse(at);
|
|
62
|
+
if (Number.isNaN(parsed)) {
|
|
63
|
+
throw new ReviewError(`${what} carries "${at}", which is not an ISO-8601 instant`);
|
|
64
|
+
}
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=review-rows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-rows.js","sourceRoot":"","sources":["../src/review-rows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;GAQG;AACH,MAAM,OAAO,WAAY,SAAQ,KAAK;IAClB,IAAI,GAAG,aAAa,CAAC;CACxC;AAID,MAAM,UAAU,IAAI,CAAC,GAAQ,EAAE,MAAc,EAAE,IAAY;IACzD,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,YAAY,MAAM,qBAAqB,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAQ,EAAE,MAAc,EAAE,IAAY;IACjE,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,YAAY,MAAM,kCAAkC,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,GAAQ,EAAE,MAAc,EAAE,IAAY;IACnE,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,YAAY,MAAM,sCAAsC,CAAC,CAAC;IACzF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,GAAQ,EAAE,MAAc,EAAE,IAAY;IAC3D,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,YAAY,MAAM,yBAAyB,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,EAAU,EAAE,IAAY;IAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC9B,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,aAAa,EAAE,qCAAqC,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * How a review operation says no, and every value that can make it say so.\n *\n * `ReviewError` is a leaf on purpose: the store, the reader and the writer all\n * throw it, and a class everything imports cannot live in a file that imports\n * them back. The readers sit with it because they are where most of these\n * refusals come from — a stored row is validated rather than cast, for the same\n * reason [`history-rows.ts`](./history-rows.ts) does it — and `instant` is that\n * refusal applied to a timestamp on the way in rather than to a column on the way\n * out.\n */\n\n/**\n * A review operation that cannot be carried out, as distinct from a platform\n * that could not be reached.\n *\n * Separate from `RasterStoreError` because they route differently: this is a 4xx\n * — the caller asked for something that is not so — and a store failure is a 5xx\n * and a stopped run. A single error type would make an operator's dashboard show\n * \"Cloudflare is down\" for a reviewer who clicked approve on the wrong subject.\n */\nexport class ReviewError extends Error {\n override readonly name = 'ReviewError';\n}\n\nexport type Row = Record<string, unknown>;\n\nexport function text(row: Row, column: string, what: string): string {\n const value = row[column];\n if (typeof value !== 'string') {\n throw new ReviewError(`${what} has a \\`${column}\\` that is not text`);\n }\n return value;\n}\n\nexport function 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 ReviewError(`${what} has a \\`${column}\\` that is neither text nor null`);\n }\n return value;\n}\n\nexport function optionalNumber(row: Row, column: string, what: string): number | undefined {\n const value = row[column];\n if (value === null || value === undefined) return undefined;\n if (typeof value === 'bigint') return Number(value);\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n throw new ReviewError(`${what} has a \\`${column}\\` that is neither a number nor null`);\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 ReviewError(`${what} has a \\`${column}\\` that is not a number`);\n }\n return value;\n}\n\nexport function instant(at: string, what: string): number {\n const parsed = Date.parse(at);\n if (Number.isNaN(parsed)) {\n throw new ReviewError(`${what} carries \"${at}\", which is not an ISO-8601 instant`);\n }\n return parsed;\n}\n"]}
|