browser-broker 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/.env.example +173 -0
- package/LICENSE +21 -0
- package/README.md +374 -0
- package/RELEASES.md +97 -0
- package/dist/package.json +58 -0
- package/dist/src/adapter/conformance/case.js +1 -0
- package/dist/src/adapter/conformance/cases.js +429 -0
- package/dist/src/adapter/conformance/discovery.js +156 -0
- package/dist/src/adapter/conformance/driver.js +1 -0
- package/dist/src/adapter/conformance/drivers.js +36 -0
- package/dist/src/adapter/conformance/run.js +224 -0
- package/dist/src/adapter/conformance/service-subject.js +165 -0
- package/dist/src/adapter/contract.js +24 -0
- package/dist/src/adapter/operations.js +114 -0
- package/dist/src/adapter/service-seam.js +1 -0
- package/dist/src/artifacts/names.js +229 -0
- package/dist/src/artifacts/store.js +174 -0
- package/dist/src/bin/broker-tool.js +63 -0
- package/dist/src/bin/broker.js +111 -0
- package/dist/src/browser/adoption.js +143 -0
- package/dist/src/browser/automation-probe.js +113 -0
- package/dist/src/browser/conformance/case.js +1 -0
- package/dist/src/browser/conformance/cases.js +192 -0
- package/dist/src/browser/conformance/run.js +102 -0
- package/dist/src/browser/conformance/subjects.js +19 -0
- package/dist/src/browser/discovery.js +226 -0
- package/dist/src/browser/driver.js +195 -0
- package/dist/src/browser/fake.js +585 -0
- package/dist/src/browser/launch.js +504 -0
- package/dist/src/browser/real.js +1425 -0
- package/dist/src/browser/setup.js +161 -0
- package/dist/src/capture/accounting.js +59 -0
- package/dist/src/capture/image.js +112 -0
- package/dist/src/capture/ladder.js +72 -0
- package/dist/src/capture/legibility.js +195 -0
- package/dist/src/capture/pipeline.js +153 -0
- package/dist/src/capture/tiers.js +166 -0
- package/dist/src/cli/adapter.js +233 -0
- package/dist/src/cli/commands.js +270 -0
- package/dist/src/cli/conformance-driver.js +119 -0
- package/dist/src/cli/diffs.js +122 -0
- package/dist/src/cli/image.js +274 -0
- package/dist/src/cli/index.js +895 -0
- package/dist/src/cli/login-command.js +401 -0
- package/dist/src/cli/operations-commands.js +186 -0
- package/dist/src/cli/reconcile-command.js +137 -0
- package/dist/src/cli/sign-in.js +134 -0
- package/dist/src/cli/telemetry.js +222 -0
- package/dist/src/config/environment.js +446 -0
- package/dist/src/diff/artifact-path.js +77 -0
- package/dist/src/diff/crops.js +102 -0
- package/dist/src/diff/geometry.js +122 -0
- package/dist/src/diff/image.js +132 -0
- package/dist/src/diff/mask.js +46 -0
- package/dist/src/diff/regions.js +263 -0
- package/dist/src/diff/settings.js +135 -0
- package/dist/src/doctor/checks.js +588 -0
- package/dist/src/doctor/report.js +152 -0
- package/dist/src/doctor/session.js +161 -0
- package/dist/src/errors.js +36 -0
- package/dist/src/feedback/read.js +119 -0
- package/dist/src/feedback/record.js +199 -0
- package/dist/src/operations/addresses.js +175 -0
- package/dist/src/operations/derive.js +109 -0
- package/dist/src/operations/ledger.js +194 -0
- package/dist/src/operations/status.js +197 -0
- package/dist/src/operations/telemetry.js +280 -0
- package/dist/src/report/document.js +419 -0
- package/dist/src/report/escape.js +68 -0
- package/dist/src/report/snapshot.js +97 -0
- package/dist/src/service/arbitration.js +537 -0
- package/dist/src/service/artifacts.js +85 -0
- package/dist/src/service/bridge.js +577 -0
- package/dist/src/service/broker.js +120 -0
- package/dist/src/service/browser-session.js +269 -0
- package/dist/src/service/capacity.js +62 -0
- package/dist/src/service/capture-seam.js +83 -0
- package/dist/src/service/capture-store.js +91 -0
- package/dist/src/service/comparison-store.js +101 -0
- package/dist/src/service/comparison.js +173 -0
- package/dist/src/service/events.js +93 -0
- package/dist/src/service/keys.js +68 -0
- package/dist/src/service/leases.js +147 -0
- package/dist/src/service/nudge.js +66 -0
- package/dist/src/service/operations/claim.js +692 -0
- package/dist/src/service/operations/give-back.js +131 -0
- package/dist/src/service/operations/pages.js +771 -0
- package/dist/src/service/operations/sign-in.js +915 -0
- package/dist/src/service/operations/status.js +62 -0
- package/dist/src/service/ownership.js +93 -0
- package/dist/src/service/pages.js +616 -0
- package/dist/src/service/pending-seeds.js +20 -0
- package/dist/src/service/queue.js +233 -0
- package/dist/src/service/reconcile.js +220 -0
- package/dist/src/service/refusals.js +262 -0
- package/dist/src/service/runtime.js +131 -0
- package/dist/src/service/signin-recovery.js +148 -0
- package/dist/src/service/storage-seed.js +239 -0
- package/dist/src/service/tabs.js +123 -0
- package/dist/src/store/budget.js +99 -0
- package/dist/src/store/location.js +42 -0
- package/dist/src/store/network-path.js +182 -0
- package/dist/src/store/network-volume.js +92 -0
- package/dist/src/store/open.js +226 -0
- package/dist/src/store/schema/step-001-initial.js +523 -0
- package/dist/src/store/schema/step-002-tab-budget.js +53 -0
- package/dist/src/store/schema/step-003-queue-order.js +110 -0
- package/dist/src/store/schema/step-004-tab-never-opened.js +100 -0
- package/dist/src/store/schema/step-005-storage-seed-event.js +90 -0
- package/dist/src/store/schema/step-006-signin-events.js +104 -0
- package/dist/src/store/schema/step-007-signin-without-process.js +92 -0
- package/dist/src/store/schema/step-008-signin-owner.js +76 -0
- package/dist/src/store/schema/step-009-named-browsers.js +138 -0
- package/dist/src/store/schema/step-010-signin-request.js +135 -0
- package/dist/src/store/schema/step.js +172 -0
- package/dist/src/store/schema/steps.js +58 -0
- package/dist/src/store/transaction.js +37 -0
- package/dist/src/tool/adapter.js +90 -0
- package/dist/src/tool/conformance-driver.js +184 -0
- package/dist/src/tool/protocol.js +310 -0
- package/dist/src/tool/session.js +351 -0
- package/dist/src/tool/tools.js +310 -0
- package/package.json +58 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { estimateTokens } from "../capture/tiers.js";
|
|
2
|
+
import { clampLimit } from "./ledger.js";
|
|
3
|
+
/**
|
|
4
|
+
* The capture telemetry rollups (`MILESTONES.md` #37, `SCHEMA.md` §1.7, §1.9).
|
|
5
|
+
*
|
|
6
|
+
* What pictures cost, and what diffs did — read back from the two tables that
|
|
7
|
+
* record it. This is the **reader** half of the measurement work. The study
|
|
8
|
+
* that compares one policy against another over real review work is a
|
|
9
|
+
* different thing and is not here (#37a): a study needs traffic to exist
|
|
10
|
+
* before it can be run, whereas these reads are what make the capture policy's
|
|
11
|
+
* effect legible at all, and they stand on their own the moment a single
|
|
12
|
+
* capture has been taken.
|
|
13
|
+
*
|
|
14
|
+
* ── The shape of these queries was decided by the indexes ────────────────
|
|
15
|
+
*
|
|
16
|
+
* This is worth stating plainly, because it is the constraint that chose every
|
|
17
|
+
* grouping below. `step-001-initial.ts` creates three indexes whose comments
|
|
18
|
+
* name this row's job before this row existed:
|
|
19
|
+
*
|
|
20
|
+
* | Index | Its own comment |
|
|
21
|
+
* |---|---|
|
|
22
|
+
* | `captures_taken_at (taken_at)` | "Listing, and the rollup." |
|
|
23
|
+
* | `comparisons_source (source_capture_id, at DESC)` | "The diffs run from one capture" |
|
|
24
|
+
* | `comparisons_target (target_capture_id)` | "and the diffs run against one — which is what tuning reads." |
|
|
25
|
+
*
|
|
26
|
+
* So a window over `taken_at` is an index range scan, and both directions of
|
|
27
|
+
* the diff question are single-key lookups. **A rollup that forced a full scan
|
|
28
|
+
* when an index for it existed would be the wrong shape**, and a rollup that
|
|
29
|
+
* needed an index that does not exist would be a reason to re-read the schema
|
|
30
|
+
* rather than to add one.
|
|
31
|
+
*
|
|
32
|
+
* The two groupings — `tier` and `kind` — are deliberately *not* indexed and
|
|
33
|
+
* are deliberately still here. Both are enum columns of three values each
|
|
34
|
+
* (§1.7), so grouping them is a pass over rows the window has already narrowed
|
|
35
|
+
* to, not a scan of the table. An index on a three-value column would be a
|
|
36
|
+
* write cost on every capture buying nothing a range scan does not already
|
|
37
|
+
* have.
|
|
38
|
+
*
|
|
39
|
+
* ── Read-only, and it does not sweep ────────────────────────────────────
|
|
40
|
+
*
|
|
41
|
+
* The same standing that `ledger.ts` claims and for the same reason: these
|
|
42
|
+
* decide nothing, grant nothing and refuse nothing, so they are not
|
|
43
|
+
* arbitration paths and are not entitled to expire anybody's lease (§5.2).
|
|
44
|
+
* Both tables record things that happened at a moment that has passed, so
|
|
45
|
+
* unlike `claims` there is nothing here to derive — a capture's size does not
|
|
46
|
+
* change because a lease lapsed.
|
|
47
|
+
*
|
|
48
|
+
* ── Estimated tokens are computed, never stored ─────────────────────────
|
|
49
|
+
*
|
|
50
|
+
* §1.7 deletes an `estimated_tokens` column on purpose and says why: it "is a
|
|
51
|
+
* calculation over two columns on the same row", and freezing it into a column
|
|
52
|
+
* would let it disagree with the dimensions beside it. The same section then
|
|
53
|
+
* requires it "appears on every capture response and **every rollup**". So it
|
|
54
|
+
* is computed here, by {@link estimateTokens}, from the dimensions the rows
|
|
55
|
+
* carry — one formula in `tiers.ts`, used by the capture path and by this
|
|
56
|
+
* reader, with no second copy to drift.
|
|
57
|
+
*/
|
|
58
|
+
/** The resolution rungs, exactly as the schema's check constraint spells them (§1.7). */
|
|
59
|
+
export const CAPTURE_TIERS = ['default', 'detail', 'max'];
|
|
60
|
+
/** The kinds of picture, exactly as the schema's check constraint spells them (§1.7). */
|
|
61
|
+
export const CAPTURE_KINDS = ['viewport', 'element', 'full_page'];
|
|
62
|
+
/**
|
|
63
|
+
* Build the `WHERE` clause for a capture window.
|
|
64
|
+
*
|
|
65
|
+
* **Every caller value is a bound parameter and none is interpolated**, on the
|
|
66
|
+
* same structural argument `ledger.ts` makes: the only strings reaching the SQL
|
|
67
|
+
* text are column names written in this file, so there is no path that
|
|
68
|
+
* concatenates a caller's string into a statement and nothing to review for
|
|
69
|
+
* whether a particular value was escaped.
|
|
70
|
+
*/
|
|
71
|
+
function buildCaptureConditions(window) {
|
|
72
|
+
const clauses = [];
|
|
73
|
+
const parameters = [];
|
|
74
|
+
if (window.since !== undefined) {
|
|
75
|
+
clauses.push('taken_at >= ?');
|
|
76
|
+
parameters.push(window.since);
|
|
77
|
+
}
|
|
78
|
+
if (window.until !== undefined) {
|
|
79
|
+
clauses.push('taken_at < ?');
|
|
80
|
+
parameters.push(window.until);
|
|
81
|
+
}
|
|
82
|
+
if (window.claimId !== undefined) {
|
|
83
|
+
clauses.push('claim_id = ?');
|
|
84
|
+
parameters.push(window.claimId);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
sql: clauses.length === 0 ? '' : ` WHERE ${clauses.join(' AND ')}`,
|
|
88
|
+
parameters,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Fold rows into a cost, applying the per-row token formula as it goes. */
|
|
92
|
+
function toCost(rows) {
|
|
93
|
+
let bytes = 0;
|
|
94
|
+
let estimatedTokens = 0;
|
|
95
|
+
let downscaled = 0;
|
|
96
|
+
let warned = 0;
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
bytes += row.bytes;
|
|
99
|
+
estimatedTokens += estimateTokens(row.width, row.height);
|
|
100
|
+
if (row.width !== row.source_width || row.height !== row.source_height) {
|
|
101
|
+
downscaled += 1;
|
|
102
|
+
}
|
|
103
|
+
if (row.warned === 1) {
|
|
104
|
+
warned += 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { captures: rows.length, bytes, estimatedTokens, downscaled, warned };
|
|
108
|
+
}
|
|
109
|
+
/** Split rows by their grouping column, preserving the order SQLite returned. */
|
|
110
|
+
function toGroups(rows) {
|
|
111
|
+
const buckets = new Map();
|
|
112
|
+
for (const row of rows) {
|
|
113
|
+
const bucket = buckets.get(row.grouped);
|
|
114
|
+
if (bucket === undefined) {
|
|
115
|
+
buckets.set(row.grouped, [row]);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
bucket.push(row);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return ([...buckets.entries()]
|
|
122
|
+
.map(([group, groupRows]) => ({ group, ...toCost(groupRows) }))
|
|
123
|
+
// Biggest first, because the question a breakdown answers is "what is this
|
|
124
|
+
// costing me" and the answer is at the top. Ties break by name so the
|
|
125
|
+
// ordering is total and two runs over the same data render identically.
|
|
126
|
+
.sort((a, b) => b.estimatedTokens - a.estimatedTokens || a.group.localeCompare(b.group)));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The most escalation reasons one rollup will return.
|
|
130
|
+
*
|
|
131
|
+
* Bounded for the reason `ledger.ts` bounds its slice: both tables grow without
|
|
132
|
+
* limit and one consumer of this is assembling output a person reads. The
|
|
133
|
+
* counts above are unbounded because a count of a million rows is still one
|
|
134
|
+
* number; a *list* of a million reasons is not.
|
|
135
|
+
*/
|
|
136
|
+
export const MAXIMUM_ESCALATION_REASONS = 50;
|
|
137
|
+
/**
|
|
138
|
+
* What the captures in a window cost.
|
|
139
|
+
*
|
|
140
|
+
* Three statements over one table, all sharing the window's predicate and the
|
|
141
|
+
* `captures_taken_at` index the schema created for "the rollup". Three rather
|
|
142
|
+
* than one because they answer three shapes of question — a total, two
|
|
143
|
+
* breakdowns, and a list of written reasons — and a single query returning all
|
|
144
|
+
* of them would either be a union of incompatible row shapes or a cross join
|
|
145
|
+
* that multiplies the totals by the number of groups.
|
|
146
|
+
*
|
|
147
|
+
* **The totals are not derived from the breakdowns**, though they could be.
|
|
148
|
+
* Summing `byTier` would give the same number for one query less. It is
|
|
149
|
+
* computed separately because the two are then independent readings of the
|
|
150
|
+
* same rows: if a tier ever appears that the breakdown does not know about,
|
|
151
|
+
* the total still counts it and the two disagree visibly, rather than the row
|
|
152
|
+
* disappearing from both.
|
|
153
|
+
*/
|
|
154
|
+
export function readCaptureRollup(db, window = {}) {
|
|
155
|
+
const conditions = buildCaptureConditions(window);
|
|
156
|
+
const totalRows = db
|
|
157
|
+
.prepare(`SELECT width, height, source_width, source_height, bytes, warned, '' AS grouped
|
|
158
|
+
FROM captures${conditions.sql}`)
|
|
159
|
+
.all(...conditions.parameters);
|
|
160
|
+
const tierRows = db
|
|
161
|
+
.prepare(`SELECT width, height, source_width, source_height, bytes, warned, tier AS grouped
|
|
162
|
+
FROM captures${conditions.sql}`)
|
|
163
|
+
.all(...conditions.parameters);
|
|
164
|
+
const kindRows = db
|
|
165
|
+
.prepare(`SELECT width, height, source_width, source_height, bytes, warned, kind AS grouped
|
|
166
|
+
FROM captures${conditions.sql}`)
|
|
167
|
+
.all(...conditions.parameters);
|
|
168
|
+
const reasonWhere = conditions.sql === ''
|
|
169
|
+
? ' WHERE reason IS NOT NULL'
|
|
170
|
+
: `${conditions.sql} AND reason IS NOT NULL`;
|
|
171
|
+
const reasonRows = db
|
|
172
|
+
.prepare(`SELECT id, taken_at, reason FROM captures${reasonWhere}
|
|
173
|
+
ORDER BY taken_at DESC, id DESC LIMIT ?`)
|
|
174
|
+
.all(...conditions.parameters, MAXIMUM_ESCALATION_REASONS);
|
|
175
|
+
return {
|
|
176
|
+
window,
|
|
177
|
+
total: toCost(totalRows),
|
|
178
|
+
byTier: toGroups(tierRows),
|
|
179
|
+
byKind: toGroups(kindRows),
|
|
180
|
+
escalationReasons: reasonRows.map((row) => ({
|
|
181
|
+
captureId: row.id,
|
|
182
|
+
takenAt: row.taken_at,
|
|
183
|
+
reason: row.reason,
|
|
184
|
+
})),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** Fold comparison rows into outcomes plus their distinct settings triples. */
|
|
188
|
+
function toOutcomes(rows) {
|
|
189
|
+
let changed = 0;
|
|
190
|
+
let truncated = 0;
|
|
191
|
+
// Insertion-ordered, and the query returns most recent first, so the
|
|
192
|
+
// settings come out in the order they were last used.
|
|
193
|
+
const settings = new Map();
|
|
194
|
+
for (const row of rows) {
|
|
195
|
+
if (row.changed === 1) {
|
|
196
|
+
changed += 1;
|
|
197
|
+
}
|
|
198
|
+
if (row.truncated === 1) {
|
|
199
|
+
truncated += 1;
|
|
200
|
+
}
|
|
201
|
+
const key = `${String(row.colour_tolerance)}|${String(row.minimum_region_area)}|${String(row.maximum_regions)}`;
|
|
202
|
+
const existing = settings.get(key);
|
|
203
|
+
if (existing === undefined) {
|
|
204
|
+
settings.set(key, { row, comparisons: 1, changed: row.changed === 1 ? 1 : 0 });
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
existing.comparisons += 1;
|
|
208
|
+
existing.changed += row.changed === 1 ? 1 : 0;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
comparisons: rows.length,
|
|
213
|
+
changed,
|
|
214
|
+
truncated,
|
|
215
|
+
settings: [...settings.values()].map((entry) => ({
|
|
216
|
+
colourTolerance: entry.row.colour_tolerance,
|
|
217
|
+
minimumRegionArea: entry.row.minimum_region_area,
|
|
218
|
+
maximumRegions: entry.row.maximum_regions,
|
|
219
|
+
comparisons: entry.comparisons,
|
|
220
|
+
changed: entry.changed,
|
|
221
|
+
})),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const DIFF_COLUMNS = 'colour_tolerance, minimum_region_area, maximum_regions, changed, truncated, at';
|
|
225
|
+
/**
|
|
226
|
+
* What diffs did around one capture, in both directions.
|
|
227
|
+
*
|
|
228
|
+
* **Two single-key lookups, one per index.** `comparisons_source` is
|
|
229
|
+
* `(source_capture_id, at DESC)`, so the source side gets its ordering from
|
|
230
|
+
* the index rather than from a sort; `comparisons_target` is the target key
|
|
231
|
+
* alone, so that side is a key lookup and an ordering the query asks for.
|
|
232
|
+
* Reading both directions in one statement would mean an `OR` across two
|
|
233
|
+
* different indexes, which is the shape that turns two lookups into a scan.
|
|
234
|
+
*
|
|
235
|
+
* `limit` bounds the rows each direction folds, defaulted and clamped by
|
|
236
|
+
* `ledger.ts`'s {@link clampLimit} — reused rather than restated, so the page
|
|
237
|
+
* sizes of the two readers in this directory cannot drift apart.
|
|
238
|
+
*/
|
|
239
|
+
export function readCaptureDiffActivity(db, captureId, limit) {
|
|
240
|
+
const bound = clampLimit(limit);
|
|
241
|
+
const sourceRows = db
|
|
242
|
+
.prepare(`SELECT ${DIFF_COLUMNS} FROM comparisons WHERE source_capture_id = ?
|
|
243
|
+
ORDER BY at DESC, id DESC LIMIT ?`)
|
|
244
|
+
.all(captureId, bound);
|
|
245
|
+
const targetRows = db
|
|
246
|
+
.prepare(`SELECT ${DIFF_COLUMNS} FROM comparisons WHERE target_capture_id = ?
|
|
247
|
+
ORDER BY at DESC, id DESC LIMIT ?`)
|
|
248
|
+
.all(captureId, bound);
|
|
249
|
+
return {
|
|
250
|
+
captureId,
|
|
251
|
+
asSource: toOutcomes(sourceRows),
|
|
252
|
+
asTarget: toOutcomes(targetRows),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
export function readMostDiffedTargets(db, limit) {
|
|
256
|
+
const bound = clampLimit(limit);
|
|
257
|
+
const rows = db
|
|
258
|
+
.prepare(`SELECT c.target_capture_id AS capture_id,
|
|
259
|
+
COUNT(*) AS comparisons,
|
|
260
|
+
SUM(c.changed) AS changed,
|
|
261
|
+
p.url AS url,
|
|
262
|
+
p.taken_at AS taken_at
|
|
263
|
+
FROM comparisons c
|
|
264
|
+
-- A left join, not an inner one: a comparison naming a capture whose
|
|
265
|
+
-- row is gone is still a comparison that happened, and dropping it
|
|
266
|
+
-- would quietly shrink a count whose whole purpose is to be compared
|
|
267
|
+
-- against another count.
|
|
268
|
+
LEFT JOIN captures p ON p.id = c.target_capture_id
|
|
269
|
+
GROUP BY c.target_capture_id
|
|
270
|
+
ORDER BY comparisons DESC, c.target_capture_id ASC
|
|
271
|
+
LIMIT ?`)
|
|
272
|
+
.all(bound);
|
|
273
|
+
return rows.map((row) => ({
|
|
274
|
+
captureId: row.capture_id,
|
|
275
|
+
comparisons: row.comparisons,
|
|
276
|
+
changed: row.changed,
|
|
277
|
+
url: row.url,
|
|
278
|
+
takenAt: row.taken_at,
|
|
279
|
+
}));
|
|
280
|
+
}
|