Viewing files at commit faa835d7e15eCommit detailsView latest

cloudflare/worker.js 708 lines · 24.6 KB · javascript
1
import {
2
  computeAggregates,
3
  computeComparisons,
4
  effectiveSubject,
5
  effectiveSubjectKind,
6
  rowsToMeasurements,
7
  scorecardFromCells,
8
  sha256Hex,
9
  stableId,
10
  trendSeries,
11
  upsertSubjects,
12
  verifySignature,
13
} from "./reporting.mjs";
14
15
// Re-export the pure ingest/reporting functions so tests (node --test) and
16
// any future consumers can import them straight from the worker module.
17
export {
18
  computeAggregates,
19
  computeComparisons,
20
  effectiveSubject,
21
  effectiveSubjectKind,
22
  rowsToMeasurements,
23
  scorecardFromCells,
24
  trendSeries,
25
  upsertSubjects,
26
  verifySignature,
27
};
28
29
export const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
30
31
export default {
32
  async fetch(request, env) {
33
    const url = new URL(request.url);
34
    const { pathname } = url;
35
36
    if (request.method === "GET") {
37
      if (pathname === "/health") {
38
        return json({ ok: true, service: "oak-benchmark-upload" });
39
      }
40
      if (pathname === "/api/scorecard") {
41
        return getScorecard(env);
42
      }
43
      if (pathname === "/api/trends") {
44
        return getTrends(env, url);
45
      }
46
      if (pathname.startsWith("/api/baseline-book/")) {
47
        return getBaselineBook(env, decodeURIComponent(pathname.slice("/api/baseline-book/".length)));
48
      }
49
      if (pathname === "/api/verifications") {
50
        return getVerifications(env);
51
      }
52
      return json({ ok: false, error: "not_found" }, 404);
53
    }
54
55
    if (request.method !== "POST") {
56
      return json({ ok: false, error: "not_found" }, 404);
57
    }
58
59
    if (pathname === "/webhook/github") {
60
      return handleGithubWebhook(request, env);
61
    }
62
63
    const authError = authorize(request, env);
64
    if (authError) {
65
      return authError;
66
    }
67
68
    if (pathname === "/api/scorecard") {
69
      return postScorecard(request, env);
70
    }
71
    if (pathname === "/api/baseline-book") {
72
      return postBaselineBook(request, env);
73
    }
74
    if (pathname === "/api/verifications") {
75
      return postVerification(request, env);
76
    }
77
    if (["/", "/upload", "/api/upload"].includes(pathname)) {
78
      return handleUpload(request, env);
79
    }
80
    return json({ ok: false, error: "not_found" }, 404);
81
  },
82
};
83
84
// --- Upload + ingest --------------------------------------------------------
85
86
async function handleUpload(request, env) {
87
  const contentLength = Number(request.headers.get("content-length") || "0");
88
  if (contentLength > MAX_UPLOAD_BYTES) {
89
    return json({ ok: false, error: "upload_too_large", max_bytes: MAX_UPLOAD_BYTES }, 413);
90
  }
91
92
  const body = await request.text();
93
  const bodyBytes = new TextEncoder().encode(body);
94
  if (bodyBytes.byteLength > MAX_UPLOAD_BYTES) {
95
    return json({ ok: false, error: "upload_too_large", max_bytes: MAX_UPLOAD_BYTES }, 413);
96
  }
97
98
  let parsed;
99
  try {
100
    parsed = parseJsonl(body);
101
  } catch (error) {
102
    return json({ ok: false, error: "invalid_jsonl", detail: String(error.message || error) }, 400);
103
  }
104
105
  const firstRow = parsed.firstRow;
106
  const benchId = stringValue(firstRow.bench_id);
107
  const profile = stringValue(firstRow.profile);
108
  const timestampUtc = stringValue(firstRow.timestamp_utc || firstRow.bench_id);
109
110
  if (!benchId || !profile) {
111
    return json({ ok: false, error: "missing_required_metadata", required: ["bench_id", "profile"] }, 400);
112
  }
113
114
  const sha256 = await sha256Hex(body);
115
  const r2Key = objectKey(profile, timestampUtc, benchId, sha256);
116
  const now = new Date().toISOString();
117
118
  const persistence = {
119
    r2: "not_configured",
120
    d1: "not_configured",
121
  };
122
123
  if (env.OAK_BENCH_RESULTS && typeof env.OAK_BENCH_RESULTS.put === "function") {
124
    await env.OAK_BENCH_RESULTS.put(r2Key, body, {
125
      httpMetadata: { contentType: "application/x-ndjson; charset=utf-8" },
126
      customMetadata: {
127
        bench_id: benchId,
128
        profile,
129
        timestamp_utc: timestampUtc,
130
        sha256,
131
        row_count: String(parsed.rowCount),
132
        source_oak_hash: stringValue(firstRow.source && firstRow.source.oak_hash) || "",
133
        host: stringValue(firstRow.host) || "",
134
        machine: stringValue(firstRow.machine) || "",
135
      },
136
    });
137
    persistence.r2 = "stored";
138
  }
139
140
  if (env.OAK_BENCH_DB && typeof env.OAK_BENCH_DB.prepare === "function") {
141
    await persistUploadMetadata(env.OAK_BENCH_DB, {
142
      firstRow,
143
      r2Key,
144
      sha256,
145
      rowCount: parsed.rowCount,
146
      byteCount: bodyBytes.byteLength,
147
      contentType: request.headers.get("content-type") || "application/x-ndjson",
148
      now,
149
    });
150
    persistence.d1 = "upload_metadata_recorded";
151
152
    const runKey = `${benchId}:${profile}`;
153
    const ingested = await ingestRows(env.OAK_BENCH_DB, parsed.rows, runKey, profile, now);
154
    persistence.d1 = "ingested";
155
    persistence.measurements = ingested.measurements;
156
    persistence.subjects = ingested.subjects;
157
    persistence.aggregates = ingested.aggregates;
158
    persistence.comparisons = ingested.comparisons;
159
    persistence.skipped_no_subject = ingested.skipped_no_subject;
160
  }
161
162
  return json({
163
    ok: true,
164
    bench_id: benchId,
165
    profile,
166
    row_count: parsed.rowCount,
167
    sha256,
168
    r2_key: r2Key,
169
    persistence,
170
  });
171
}
172
173
/**
174
 * The four ingest steps, all computed by pure functions in reporting.mjs:
175
 * 1. Upsert run-scoped subjects keyed (subject, subject_kind, binary_sha256).
176
 * 2. Insert each JSONL row into measurements (null-preserving projection).
177
 * 3. Per-group p50/p90/p95/p99 nearest-rank latency aggregates + averages.
178
 * 4. Target-vs-git comparison rows (delta/slower_pct/ratio per reporting.py).
179
 *
180
 * Exported so tests can drive the full ingest flow against a stub D1
181
 * binding (see reporting_parity.test.mjs re-upload test).
182
 */
183
export async function ingestRows(db, rows, runKey, profile, now) {
184
  const firstRow = rows[0] || {};
185
  const subjectDetails = firstRow.subject_details && typeof firstRow.subject_details === "object"
186
    ? firstRow.subject_details
187
    : {};
188
  const subjectVersions = firstRow.subject_versions && typeof firstRow.subject_versions === "object"
189
    ? firstRow.subject_versions
190
    : {};
191
192
  // Step 1: subjects. Identity is the deterministic subject_id, which
193
  // stableId-encodes (run_key, effective subject, kind, binary_sha256): two
194
  // binary shas for one subject name in a run stay two distinct subject
195
  // records (schema_v2.sql documents that schema.sql's inline
196
  // UNIQUE(run_key, name) is superseded by subject_id identity). Re-uploads
197
  // hash to the same id and take the conflict path, so no read-back remap is
198
  // needed. Rows with no subject identity (no subject AND no platform) are
199
  // counted in skipped_no_subject, never silently dropped.
200
  const { records: subjects, idsByKey, skipped } = await upsertSubjects(rows, runKey, subjectDetails, subjectVersions);
201
  await batched(db, subjects.map((subject) => db.prepare(`
202
    INSERT INTO subjects (subject_id, run_key, name, kind, label, version, binary_path, metadata_json)
203
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
204
    ON CONFLICT(subject_id) DO UPDATE SET
205
      name = excluded.name,
206
      kind = excluded.kind,
207
      label = excluded.label,
208
      version = excluded.version,
209
      binary_path = excluded.binary_path,
210
      metadata_json = excluded.metadata_json
211
  `).bind(
212
    subject.subject_id,
213
    subject.run_key,
214
    subject.name,
215
    subject.kind,
216
    subject.label,
217
    subject.version,
218
    subject.binary_path,
219
    subject.metadata_json,
220
  )));
221
222
  // Step 2: measurements (idempotent re-upload: replace this run's rows).
223
  // Derived tables (aggregates, comparisons) are deleted alongside the
224
  // measurements: they are recomputed from this upload's measurements, so a
225
  // re-upload with FEWER groups must not leave stale rows from a previous
226
  // upload behind (upserts alone only overwrite groups that still exist).
227
  const ingestReport = { skipped_no_subject: skipped };
228
  const measurements = rowsToMeasurements(rows, runKey, now, idsByKey, ingestReport);
229
  const aggregates = computeAggregates(measurements, profile, now);
230
  const comparisons = computeComparisons(aggregates);
231
232
  // Keep the destructive run rewrite in one D1 batch. D1 batches are
233
  // transactional, so a mid-ingest insert failure must not commit only the
234
  // deletes and leave the run empty or partially rebuilt. Subject upserts
235
  // above are non-destructive and can safely remain outside this rewrite.
236
  const runRewriteStatements = [
237
    db.prepare("DELETE FROM measurements WHERE run_key = ?").bind(runKey),
238
    db.prepare("DELETE FROM aggregates WHERE run_key = ?").bind(runKey),
239
    db.prepare("DELETE FROM comparisons WHERE run_key = ?").bind(runKey),
240
    ...measurements.map((m) => db.prepare(`
241
    INSERT INTO measurements (
242
      run_key, subject_id, scenario, operation, run_index, is_canonical,
243
      elapsed_ms, estimated_tokens_total, estimated_tokens_input, estimated_tokens_output,
244
      token_source, raw_output_bytes, stdout_bytes, stderr_bytes, output_truncated,
245
      tool_call_count, vcs_tool_call_count, terminal_tool_call_count, returncode,
246
      command_json, stderr, row_json, created_at,
247
      runner_id, runner_class, load_tier, netshape_profile
248
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
249
  `).bind(
250
    m.run_key, m.subject_id, m.scenario, m.operation, m.run_index, m.is_canonical,
251
    m.elapsed_ms, m.estimated_tokens_total, m.estimated_tokens_input, m.estimated_tokens_output,
252
    m.token_source, m.raw_output_bytes, m.stdout_bytes, m.stderr_bytes, m.output_truncated,
253
    m.tool_call_count, m.vcs_tool_call_count, m.terminal_tool_call_count, m.returncode,
254
    m.command_json, m.stderr, m.row_json, m.created_at,
255
    m.runner_id, m.runner_class, m.load_tier, m.netshape_profile,
256
  )),
257
    // Step 3: aggregates (plain INSERTs: the run's rows are deleted in the
258
    // same batch, so there is nothing to conflict with after commit).
259
    ...aggregates.map((a) => db.prepare(`
260
    INSERT INTO aggregates (
261
      run_key, subject_id, profile, scenario, operation, is_canonical,
262
      sample_count, failure_count, failure_rate,
263
      p50_ms, p90_ms, p95_ms, p99_ms, mean_ms, min_ms, max_ms,
264
      avg_tokens, avg_output_bytes, truncated_count, avg_tool_calls, updated_at
265
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
266
  `).bind(
267
    a.run_key, a.subject_id, a.profile, a.scenario, a.operation, a.is_canonical,
268
    a.sample_count, a.failure_count, a.failure_rate,
269
    a.p50_ms, a.p90_ms, a.p95_ms, a.p99_ms, a.mean_ms, a.min_ms, a.max_ms,
270
    a.avg_tokens, a.avg_output_bytes, a.truncated_count, a.avg_tool_calls, a.updated_at,
271
  )),
272
    // Step 4: comparisons (plain INSERTs, same reasoning as step 3).
273
    ...comparisons.map((c) => db.prepare(`
274
    INSERT INTO comparisons (
275
      run_key, target_subject_id, baseline_subject_id, baseline_kind, profile,
276
      scenario, operation,
277
      p50_delta_ms, p50_slower_pct, p90_delta_ms, p90_slower_pct,
278
      p99_delta_ms, p99_slower_pct,
279
      avg_token_delta, token_higher_pct, avg_output_byte_delta, output_byte_higher_pct,
280
      avg_tool_call_delta, tool_call_higher_pct, failure_rate_delta_pp,
281
      threshold_pct, min_delta_ms, is_regression, regression_dimensions, state, severity_score, updated_at
282
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
283
  `).bind(
284
    c.run_key, c.target_subject_id, c.baseline_subject_id, c.baseline_kind, c.profile,
285
    c.scenario, c.operation,
286
    c.p50_delta_ms, c.p50_slower_pct, c.p90_delta_ms, c.p90_slower_pct,
287
    c.p99_delta_ms, c.p99_slower_pct,
288
    c.avg_token_delta, c.token_higher_pct, c.avg_output_byte_delta, c.output_byte_higher_pct,
289
    c.avg_tool_call_delta, c.tool_call_higher_pct, c.failure_rate_delta_pp,
290
    c.threshold_pct, c.min_delta_ms, c.is_regression,
291
    c.regression_dimensions == null ? null : JSON.stringify(c.regression_dimensions),
292
    c.state, c.severity_score, c.updated_at ?? now,
293
  )),
294
  ];
295
296
  await db.batch(runRewriteStatements);
297
298
  return {
299
    subjects: subjects.length,
300
    measurements: measurements.length,
301
    aggregates: aggregates.length,
302
    comparisons: comparisons.length,
303
    skipped_no_subject: ingestReport.skipped_no_subject,
304
  };
305
}
306
307
async function batched(db, statements, chunkSize = 50) {
308
  for (let index = 0; index < statements.length; index += chunkSize) {
309
    const chunk = statements.slice(index, index + chunkSize);
310
    if (chunk.length) {
311
      await db.batch(chunk);
312
    }
313
  }
314
}
315
316
// --- API endpoints (JSON; public pages consume these) -----------------------
317
318
async function getScorecard(env) {
319
  const db = requireDb(env);
320
  if (!db) {
321
    return json({ ok: false, error: "d1_not_configured" }, 503);
322
  }
323
  const row = await db.prepare(
324
    "SELECT bench_id, profile, scorecard_json, created_utc FROM scorecards ORDER BY id DESC LIMIT 1",
325
  ).first();
326
  if (!row) {
327
    return json({ ok: false, error: "no_scorecard" }, 404);
328
  }
329
  const scorecard = JSON.parse(row.scorecard_json);
330
  const cells = Array.isArray(scorecard) ? scorecard : scorecard.cells || scorecard.targets || [];
331
  return json({
332
    ok: true,
333
    bench_id: row.bench_id,
334
    profile: row.profile,
335
    created_utc: row.created_utc,
336
    summary: scorecardFromCells(cells),
337
    scorecard,
338
  });
339
}
340
341
async function postScorecard(request, env) {
342
  const db = requireDb(env);
343
  if (!db) {
344
    return json({ ok: false, error: "d1_not_configured" }, 503);
345
  }
346
  const payload = await readJson(request);
347
  if (payload instanceof Response) {
348
    return payload;
349
  }
350
  const now = new Date().toISOString();
351
  await db.prepare(
352
    "INSERT INTO scorecards (bench_id, profile, scorecard_json, created_utc) VALUES (?, ?, ?, ?)",
353
  ).bind(
354
    nullableString(payload.bench_id),
355
    nullableString(payload.profile),
356
    JSON.stringify(payload.scorecard ?? payload),
357
    now,
358
  ).run();
359
  return json({ ok: true, created_utc: now });
360
}
361
362
async function getTrends(env, url) {
363
  const db = requireDb(env);
364
  if (!db) {
365
    return json({ ok: false, error: "d1_not_configured" }, 503);
366
  }
367
  const scenario = url.searchParams.get("scenario");
368
  const operation = url.searchParams.get("operation");
369
  const metric = url.searchParams.get("metric") || "p50";
370
  if (!scenario || !operation) {
371
    return json({ ok: false, error: "missing_params", required: ["scenario", "operation"] }, 400);
372
  }
373
  const rows = await db.prepare(`
374
    SELECT r.timestamp_utc, a.run_key, s.name AS subject, a.scenario, a.operation,
375
           a.p50_ms, a.p90_ms, a.p95_ms, a.p99_ms, a.mean_ms,
376
           a.avg_tokens, a.avg_output_bytes, a.avg_tool_calls, a.failure_rate
377
    FROM aggregates a
378
    JOIN benchmark_runs r ON r.run_key = a.run_key
379
    JOIN subjects s ON s.subject_id = a.subject_id
380
    WHERE a.scenario = ? AND a.operation = ?
381
    ORDER BY r.timestamp_utc ASC
382
    LIMIT 2000
383
  `).bind(scenario, operation).all();
384
  const series = trendSeries(rows.results || [], { scenario, operation, metric });
385
  if (!series) {
386
    return json({ ok: false, error: "unknown_metric", metric }, 400);
387
  }
388
  return json({ ok: true, ...series });
389
}
390
391
async function getBaselineBook(env, bookId) {
392
  const db = requireDb(env);
393
  if (!db) {
394
    return json({ ok: false, error: "d1_not_configured" }, 503);
395
  }
396
  if (!bookId) {
397
    return json({ ok: false, error: "missing_book_id" }, 400);
398
  }
399
  const row = await db.prepare(
400
    "SELECT book_id, runner_class, manifest_json, book_json, created_utc FROM baseline_books WHERE book_id = ?",
401
  ).bind(bookId).first();
402
  if (!row) {
403
    return json({ ok: false, error: "not_found", book_id: bookId }, 404);
404
  }
405
  return json({
406
    ok: true,
407
    book_id: row.book_id,
408
    runner_class: row.runner_class,
409
    created_utc: row.created_utc,
410
    manifest: row.manifest_json ? JSON.parse(row.manifest_json) : null,
411
    book: JSON.parse(row.book_json),
412
  });
413
}
414
415
async function postBaselineBook(request, env) {
416
  const db = requireDb(env);
417
  if (!db) {
418
    return json({ ok: false, error: "d1_not_configured" }, 503);
419
  }
420
  const payload = await readJson(request);
421
  if (payload instanceof Response) {
422
    return payload;
423
  }
424
  const bookId = stringValue(payload.book_id);
425
  if (!bookId || typeof payload.book !== "object" || payload.book === null) {
426
    return json({ ok: false, error: "missing_required_fields", required: ["book_id", "book"] }, 400);
427
  }
428
  const now = new Date().toISOString();
429
  await db.prepare(`
430
    INSERT INTO baseline_books (book_id, runner_class, manifest_json, book_json, created_utc)
431
    VALUES (?, ?, ?, ?, ?)
432
    ON CONFLICT(book_id) DO UPDATE SET
433
      runner_class = excluded.runner_class,
434
      manifest_json = excluded.manifest_json,
435
      book_json = excluded.book_json,
436
      created_utc = excluded.created_utc
437
  `).bind(
438
    bookId,
439
    nullableString(payload.runner_class),
440
    payload.manifest === undefined || payload.manifest === null ? null : JSON.stringify(payload.manifest),
441
    JSON.stringify(payload.book),
442
    now,
443
  ).run();
444
  return json({ ok: true, book_id: bookId, created_utc: now });
445
}
446
447
async function getVerifications(env) {
448
  const db = requireDb(env);
449
  if (!db) {
450
    return json({ ok: false, error: "d1_not_configured" }, 503);
451
  }
452
  const rows = await db.prepare(
453
    "SELECT id, bundle_sha256, verdict, detail_json, created_utc FROM verifications ORDER BY id DESC LIMIT 100",
454
  ).all();
455
  return json({
456
    ok: true,
457
    verifications: (rows.results || []).map((row) => ({
458
      id: row.id,
459
      bundle_sha256: row.bundle_sha256,
460
      verdict: row.verdict,
461
      detail: row.detail_json ? JSON.parse(row.detail_json) : null,
462
      created_utc: row.created_utc,
463
    })),
464
  });
465
}
466
467
async function postVerification(request, env) {
468
  const db = requireDb(env);
469
  if (!db) {
470
    return json({ ok: false, error: "d1_not_configured" }, 503);
471
  }
472
  const payload = await readJson(request);
473
  if (payload instanceof Response) {
474
    return payload;
475
  }
476
  const bundleSha = stringValue(payload.bundle_sha256);
477
  const verdict = stringValue(payload.verdict);
478
  if (!bundleSha || !verdict) {
479
    return json({ ok: false, error: "missing_required_fields", required: ["bundle_sha256", "verdict"] }, 400);
480
  }
481
  const now = new Date().toISOString();
482
  await db.prepare(
483
    "INSERT INTO verifications (bundle_sha256, verdict, detail_json, created_utc) VALUES (?, ?, ?, ?)",
484
  ).bind(
485
    bundleSha,
486
    verdict,
487
    payload.detail === undefined || payload.detail === null ? null : JSON.stringify(payload.detail),
488
    now,
489
  ).run();
490
  return json({ ok: true, created_utc: now });
491
}
492
493
// --- GitHub webhook (HMAC-verified, recorded into webhook_events) -----------
494
495
async function handleGithubWebhook(request, env) {
496
  if (!env.GITHUB_WEBHOOK_SECRET) {
497
    return json({ ok: false, error: "webhook_not_configured" }, 503);
498
  }
499
  const body = await request.text();
500
  const valid = await verifySignature(
501
    env.GITHUB_WEBHOOK_SECRET,
502
    body,
503
    request.headers.get("x-hub-signature-256") || "",
504
  );
505
  if (!valid) {
506
    return json({ ok: false, error: "invalid_signature" }, 401);
507
  }
508
  const db = requireDb(env);
509
  if (db) {
510
    await db.prepare(
511
      "INSERT INTO webhook_events (event, delivery_id, payload_json, signature_valid, created_utc) VALUES (?, ?, ?, 1, ?)",
512
    ).bind(
513
      nullableString(request.headers.get("x-github-event")),
514
      nullableString(request.headers.get("x-github-delivery")),
515
      body,
516
      new Date().toISOString(),
517
    ).run();
518
  }
519
  return json({ ok: true, recorded: Boolean(db) });
520
}
521
522
// --- helpers -----------------------------------------------------------------
523
524
function requireDb(env) {
525
  return env.OAK_BENCH_DB && typeof env.OAK_BENCH_DB.prepare === "function" ? env.OAK_BENCH_DB : null;
526
}
527
528
async function readJson(request) {
529
  try {
530
    const payload = await request.json();
531
    if (!payload || typeof payload !== "object") {
532
      throw new Error("payload must be a JSON object");
533
    }
534
    return payload;
535
  } catch (error) {
536
    return json({ ok: false, error: "invalid_json", detail: String(error.message || error) }, 400);
537
  }
538
}
539
540
function authorize(request, env) {
541
  const uploadToken = stringValue(env.UPLOAD_TOKEN).trim();
542
  if (!uploadToken) {
543
    return json({ ok: false, error: "upload_token_not_configured" }, 503);
544
  }
545
  const expected = `Bearer ${uploadToken}`;
546
  if (request.headers.get("authorization") === expected) {
547
    return null;
548
  }
549
  return json({ ok: false, error: "unauthorized" }, 401);
550
}
551
552
function parseJsonl(text) {
553
  const lines = text.split(/\r?\n/);
554
  const rows = [];
555
556
  for (let index = 0; index < lines.length; index += 1) {
557
    const line = lines[index].trim();
558
    if (!line) {
559
      continue;
560
    }
561
    const row = JSON.parse(line);
562
    if (!row || typeof row !== "object" || Array.isArray(row)) {
563
      throw new Error(`line ${index + 1} is not a JSON object`);
564
    }
565
    rows.push(row);
566
  }
567
568
  if (rows.length === 0) {
569
    throw new Error("upload contained no JSON rows");
570
  }
571
572
  return { firstRow: rows[0], rows, rowCount: rows.length };
573
}
574
575
async function persistUploadMetadata(db, upload) {
576
  const row = upload.firstRow;
577
  const source = row.source && typeof row.source === "object" ? row.source : {};
578
  const subjectVersions = row.subject_versions && typeof row.subject_versions === "object"
579
    ? row.subject_versions
580
    : {};
581
  const subjectDetails = row.subject_details && typeof row.subject_details === "object"
582
    ? row.subject_details
583
    : {};
584
  const hostId = await stableId([
585
    stringValue(row.host),
586
    stringValue(row.platform),
587
    stringValue(row.machine),
588
    stringValue(row.python),
589
  ].join("\u001f"));
590
591
  await db.batch([
592
    db.prepare(`
593
      INSERT INTO hosts (
594
        host_id, host, platform, machine, python, metadata_json, first_seen_at, last_seen_at
595
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
596
      ON CONFLICT(host_id) DO UPDATE SET
597
        last_seen_at = excluded.last_seen_at,
598
        metadata_json = excluded.metadata_json
599
    `).bind(
600
      hostId,
601
      nullableString(row.host),
602
      nullableString(row.platform),
603
      nullableString(row.machine),
604
      nullableString(row.python),
605
      JSON.stringify({ source: "worker-upload" }),
606
      upload.now,
607
      upload.now,
608
    ),
609
    db.prepare(`
610
      INSERT INTO uploads (
611
        r2_key, sha256, bench_id, profile, row_count, byte_count, content_type, status, uploaded_at
612
      ) VALUES (?, ?, ?, ?, ?, ?, ?, 'accepted', ?)
613
      ON CONFLICT(r2_key) DO UPDATE SET
614
        row_count = excluded.row_count,
615
        byte_count = excluded.byte_count,
616
        status = excluded.status,
617
        uploaded_at = excluded.uploaded_at
618
    `).bind(
619
      upload.r2Key,
620
      upload.sha256,
621
      stringValue(row.bench_id),
622
      stringValue(row.profile),
623
      upload.rowCount,
624
      upload.byteCount,
625
      upload.contentType,
626
      upload.now,
627
    ),
628
    db.prepare(`
629
      INSERT INTO benchmark_runs (
630
        run_key, bench_id, timestamp_utc, profile, source_oak_hash, source_oak_repo,
631
        source_oak_status, host_id, raw_r2_key, benchmark_track, command_semantics_version,
632
        row_count, subject_versions_json, subject_details_json, uploaded_at
633
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
634
      ON CONFLICT(run_key) DO UPDATE SET
635
        raw_r2_key = excluded.raw_r2_key,
636
        benchmark_track = excluded.benchmark_track,
637
        command_semantics_version = excluded.command_semantics_version,
638
        row_count = excluded.row_count,
639
        subject_versions_json = excluded.subject_versions_json,
640
        subject_details_json = excluded.subject_details_json,
641
        uploaded_at = excluded.uploaded_at
642
    `).bind(
643
      `${stringValue(row.bench_id)}:${stringValue(row.profile)}`,
644
      stringValue(row.bench_id),
645
      stringValue(row.timestamp_utc || row.bench_id),
646
      stringValue(row.profile),
647
      nullableString(source.oak_hash),
648
      nullableString(source.oak_repo),
649
      nullableString(source.oak_status),
650
      hostId,
651
      upload.r2Key,
652
      nullableString(row.benchmark_track),
653
      nullableString(row.command_semantics_version),
654
      upload.rowCount,
655
      JSON.stringify(subjectVersions),
656
      JSON.stringify(subjectDetails),
657
      upload.now,
658
    ),
659
  ]);
660
}
661
662
function objectKey(profile, timestampUtc, benchId, sha256) {
663
  const date = isoDate(timestampUtc);
664
  return [
665
    "raw/v1",
666
    `profile=${sanitizePathPart(profile)}`,
667
    `date=${date}`,
668
    `bench_id=${sanitizePathPart(benchId)}`,
669
    `sha256=${sha256.slice(0, 12)}`,
670
    "results.jsonl",
671
  ].join("/");
672
}
673
674
function isoDate(timestampUtc) {
675
  const compact = String(timestampUtc || "");
676
  const match = compact.match(/^(\d{4})(\d{2})(\d{2})T/);
677
  if (match) {
678
    return `${match[1]}-${match[2]}-${match[3]}`;
679
  }
680
  const parsed = new Date(compact);
681
  if (!Number.isNaN(parsed.getTime())) {
682
    return parsed.toISOString().slice(0, 10);
683
  }
684
  return new Date().toISOString().slice(0, 10);
685
}
686
687
function sanitizePathPart(value) {
688
  return String(value || "unknown").replace(/[^A-Za-z0-9._=-]/g, "_");
689
}
690
691
function stringValue(value) {
692
  if (value === null || value === undefined) {
693
    return "";
694
  }
695
  return String(value);
696
}
697
698
function nullableString(value) {
699
  const text = stringValue(value);
700
  return text ? text : null;
701
}
702
703
function json(payload, status = 200) {
704
  return new Response(JSON.stringify(payload, null, 2), {
705
    status,
706
    headers: { "content-type": "application/json; charset=utf-8" },
707
  });
708
}