@benchsdk/runner 0.2.0 → 0.5.2

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/dist/index.cjs CHANGED
@@ -30,18 +30,37 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ AuthError: () => import_cli4.AuthError,
34
+ BENCHSDK_RUNNER_VERSION: () => BENCHSDK_RUNNER_VERSION,
35
+ BenchmarkApiError: () => import_api3.BenchmarkApiError,
36
+ BenchmarkConfigError: () => BenchmarkConfigError,
37
+ BenchmarkReporter: () => import_worker3.BenchmarkReporter,
33
38
  NoAvailableParticipantsError: () => NoAvailableParticipantsError,
39
+ ScoringSpecError: () => ScoringSpecError,
34
40
  TaskError: () => TaskError,
41
+ claimBenchmarkReporter: () => import_worker3.claimBenchmarkReporter,
42
+ createApiClient: () => import_cli4.createApiClient,
43
+ createBenchmarkClient: () => import_api3.createBenchmarkClient,
44
+ createSystemMetricsCollector: () => import_worker3.createSystemMetricsCollector,
35
45
  defineBenchmarkConfig: () => defineBenchmarkConfig,
46
+ defineOnComplete: () => defineOnComplete,
36
47
  defineTask: () => defineTask,
48
+ filterParticipantsByEnv: () => import_worker3.filterParticipantsByEnv,
37
49
  higherIsBetter: () => higherIsBetter,
38
50
  lowerIsBetter: () => lowerIsBetter,
39
51
  mergeConfig: () => mergeConfig,
40
52
  parseCliArgs: () => parseCliArgs,
53
+ resolveAuth: () => import_cli4.resolveAuth,
41
54
  run: () => run,
42
55
  runBenchmark: () => runBenchmark,
43
56
  runBenchmarkFile: () => runBenchmarkFile,
44
- score: () => score
57
+ runBenchmarkWorker: () => runBenchmarkWorker,
58
+ runWorker: () => import_worker3.runWorker,
59
+ score: () => score,
60
+ scoringConfigToSpec: () => scoringConfigToSpec,
61
+ selectParticipants: () => import_worker3.selectParticipants,
62
+ validateBenchmarkConfig: () => validateBenchmarkConfig,
63
+ validateScoringSpec: () => validateScoringSpec
45
64
  });
46
65
  module.exports = __toCommonJS(src_exports);
47
66
 
@@ -50,67 +69,231 @@ var TaskError = class extends Error {
50
69
  code;
51
70
  data;
52
71
  steps;
72
+ step;
73
+ timeoutMs;
53
74
  constructor(message, opts) {
54
75
  super(message);
55
76
  this.name = "TaskError";
56
77
  this.code = opts?.code;
57
78
  this.data = opts?.data;
58
79
  this.steps = opts?.steps;
80
+ this.step = opts?.step;
81
+ this.timeoutMs = opts?.timeoutMs;
82
+ }
83
+ toString() {
84
+ let s = `[${this.name}${this.code ? ` (${this.code})` : ""}] ${this.message}`;
85
+ if (this.step) {
86
+ s += `
87
+ step: ${this.step}`;
88
+ }
89
+ if (this.timeoutMs !== void 0) {
90
+ s += `
91
+ timeoutMs: ${this.timeoutMs}`;
92
+ }
93
+ if (this.data && Object.keys(this.data).length > 0) {
94
+ s += `
95
+ data: ${JSON.stringify(this.data, null, 2)}`;
96
+ }
97
+ return s;
98
+ }
99
+ };
100
+ var BenchmarkConfigError = class _BenchmarkConfigError extends Error {
101
+ issues;
102
+ constructor(issues) {
103
+ super(_BenchmarkConfigError.formatIssues(issues));
104
+ this.name = "BenchmarkConfigError";
105
+ this.issues = issues;
106
+ }
107
+ static formatIssues(issues) {
108
+ const lines = issues.map((i) => ` - ${i.field}: ${i.message}`);
109
+ return `Invalid benchmark config:
110
+ ${lines.join("\n")}
111
+
112
+ Fix the fields above and try again.`;
59
113
  }
60
114
  };
61
- function assertPositiveInt(value, field) {
62
- if (value === void 0) return;
63
- if (!Number.isInteger(value) || value < 1) {
64
- throw new Error(`${field} must be an integer >= 1 (got ${value})`);
115
+ function assertFiniteNumber(value, field) {
116
+ if (typeof value !== "number" || !Number.isFinite(value)) {
117
+ throw new Error(`${field} must be a finite number (got ${value})`);
65
118
  }
119
+ return value;
66
120
  }
67
- function defineBenchmarkConfig(config) {
68
- if (!config.benchmarkSlug || typeof config.benchmarkSlug !== "string") {
69
- throw new Error("benchmarkSlug is required");
121
+ function assertNonEmptyString(value, field) {
122
+ if (typeof value !== "string" || value.trim() === "") {
123
+ throw new Error(`${field} must be a non-empty string`);
70
124
  }
71
- if (!config.benchmarkName || typeof config.benchmarkName !== "string") {
72
- throw new Error("benchmarkName is required");
125
+ return value;
126
+ }
127
+ function assertOnlyAllowedKeys(value, allowed, field) {
128
+ for (const key of Object.keys(value)) {
129
+ if (!allowed.includes(key)) {
130
+ throw new Error(`${field} contains unexpected key: '${key}'`);
131
+ }
73
132
  }
74
- if (config.phases !== void 0) {
75
- if (config.iterations !== void 0) {
76
- throw new Error("phases and iterations are mutually exclusive");
133
+ }
134
+ function validateBenchmarkScoringConfig(scoring, display) {
135
+ if (!Array.isArray(scoring.metrics) || scoring.metrics.length === 0) {
136
+ throw new Error("scoring.metrics must be a non-empty array");
137
+ }
138
+ if (scoring.success !== void 0) {
139
+ const requireData = scoring.success.requireData;
140
+ if (requireData === null || typeof requireData !== "object" || Array.isArray(requireData)) {
141
+ throw new Error("scoring.success.requireData must be a plain object");
77
142
  }
78
- if (!Array.isArray(config.phases) || config.phases.length === 0) {
79
- throw new Error("phases must be a non-empty array");
143
+ if (Object.keys(requireData).length === 0) {
144
+ throw new Error("scoring.success.requireData must declare at least one data field");
80
145
  }
81
- const seen = /* @__PURE__ */ new Set();
82
- for (const phase of config.phases) {
83
- if (!phase.name || typeof phase.name !== "string") {
84
- throw new Error("each phase requires a non-empty name");
146
+ for (const [key, value] of Object.entries(requireData)) {
147
+ const type = typeof value;
148
+ if (type !== "string" && type !== "number" && type !== "boolean") {
149
+ throw new Error(
150
+ `scoring.success.requireData.${key} must be a string, number, or boolean (got ${type})`
151
+ );
85
152
  }
86
- if (seen.has(phase.name)) {
87
- throw new Error(`duplicate phase name: ${phase.name}`);
153
+ }
154
+ }
155
+ const seen = /* @__PURE__ */ new Set();
156
+ let totalWeight = 0;
157
+ for (let i = 0; i < scoring.metrics.length; i++) {
158
+ const metric = scoring.metrics[i];
159
+ if (metric === null || typeof metric !== "object" || Array.isArray(metric)) {
160
+ throw new Error(`scoring.metrics[${i}] must be an object`);
161
+ }
162
+ const key = metric.key;
163
+ if (typeof key !== "string" || key.trim() === "") {
164
+ throw new Error(`scoring.metrics[${i}].key must be a non-empty string`);
165
+ }
166
+ if (seen.has(key)) {
167
+ throw new Error(`duplicate scoring metric key: ${key}`);
168
+ }
169
+ seen.add(key);
170
+ const displayMetrics = Array.isArray(display?.metrics) ? display.metrics : void 0;
171
+ const displayMetric = displayMetrics?.find((m) => m?.key === key);
172
+ if (metric.unit !== void 0) {
173
+ if (typeof metric.unit !== "string") {
174
+ throw new Error(`scoring.metrics[${i}].unit must be a string`);
88
175
  }
89
- seen.add(phase.name);
90
- assertPositiveInt(phase.iterations, `phase '${phase.name}' iterations`);
176
+ if (displayMetric?.unit !== void 0 && metric.unit !== displayMetric.unit) {
177
+ throw new Error(
178
+ `scoring.metrics[${i}].unit '${metric.unit}' conflicts with display.metrics[${i}].unit '${displayMetric.unit}' for key '${key}'`
179
+ );
180
+ }
181
+ }
182
+ assertFiniteNumber(metric.ceiling, `scoring.metrics[${i}].ceiling`);
183
+ if (metric.floor !== void 0) {
184
+ assertFiniteNumber(metric.floor, `scoring.metrics[${i}].floor`);
185
+ }
186
+ if (metric.weights === null || typeof metric.weights !== "object" || Array.isArray(metric.weights)) {
187
+ throw new Error(`scoring.metrics[${i}].weights must be an object`);
188
+ }
189
+ const median = assertFiniteNumber(metric.weights.median, `scoring.metrics[${i}].weights.median`);
190
+ const p95 = assertFiniteNumber(metric.weights.p95, `scoring.metrics[${i}].weights.p95`);
191
+ const p99 = assertFiniteNumber(metric.weights.p99, `scoring.metrics[${i}].weights.p99`);
192
+ if (median < 0 || p95 < 0 || p99 < 0) {
193
+ throw new Error(`scoring.metrics[${i}].weights must be non-negative`);
194
+ }
195
+ totalWeight += median + p95 + p99;
196
+ if (metric.trim !== void 0) {
197
+ assertFiniteNumber(metric.trim, `scoring.metrics[${i}].trim`);
91
198
  }
92
199
  }
93
- assertPositiveInt(config.iterations, "iterations");
94
- assertPositiveInt(config.concurrency, "concurrency");
95
- if (config.staggerDelayMs !== void 0 && (!Number.isFinite(config.staggerDelayMs) || config.staggerDelayMs < 0)) {
96
- throw new Error(`staggerDelayMs must be a number >= 0 (got ${config.staggerDelayMs})`);
200
+ if (Math.abs(totalWeight - 1) > 0.01) {
201
+ throw new Error(`scoring metric weights must sum to 1.0 (got ${totalWeight.toFixed(3)})`);
97
202
  }
98
- if (config.groupBy !== void 0 && config.groupBy !== "participant" && config.groupBy !== "round") {
99
- throw new Error(`groupBy must be 'participant' or 'round' (got ${config.groupBy})`);
203
+ }
204
+ function validateBenchmarkDisplayConfig(display) {
205
+ if (typeof display !== "object" || display === null || Array.isArray(display)) {
206
+ throw new Error("display must be an object");
100
207
  }
101
- if (config.shapes !== void 0) {
102
- for (const [shapeName, shape] of Object.entries(config.shapes)) {
103
- if (!shape.slug || !/^[a-z0-9][a-z0-9-]*$/.test(shape.slug)) {
104
- throw new Error(`shape '${shapeName}' needs a lowercase slug (got ${JSON.stringify(shape.slug)})`);
208
+ assertOnlyAllowedKeys(display, ["metrics", "steps", "overview"], "display");
209
+ const displayMetricKeys = /* @__PURE__ */ new Set();
210
+ if (display.metrics !== void 0) {
211
+ if (!Array.isArray(display.metrics)) {
212
+ throw new Error("display.metrics must be an array");
213
+ }
214
+ for (let i = 0; i < display.metrics.length; i++) {
215
+ const metric = display.metrics[i];
216
+ if (metric === null || typeof metric !== "object" || Array.isArray(metric)) {
217
+ throw new Error(`display.metrics[${i}] must be an object`);
218
+ }
219
+ assertOnlyAllowedKeys(metric, ["key", "label", "unit", "direction", "decimals", "order"], `display.metrics[${i}]`);
220
+ const key = assertNonEmptyString(metric.key, `display.metrics[${i}].key`);
221
+ if (displayMetricKeys.has(key)) {
222
+ throw new Error(`duplicate display metric key: ${key}`);
105
223
  }
106
- if (shape.name !== void 0 && (typeof shape.name !== "string" || shape.name.trim() === "")) {
107
- throw new Error(`shape '${shapeName}' name must be a non-empty string`);
224
+ displayMetricKeys.add(key);
225
+ assertNonEmptyString(metric.label, `display.metrics[${i}].label`);
226
+ if (metric.unit !== void 0 && typeof metric.unit !== "string") {
227
+ throw new Error(`display.metrics[${i}].unit must be a string`);
108
228
  }
109
- if (shape.staggerDelayMs !== void 0 && (!Number.isFinite(shape.staggerDelayMs) || shape.staggerDelayMs < 0)) {
110
- throw new Error(`shape '${shapeName}' staggerDelayMs must be a number >= 0 (got ${shape.staggerDelayMs})`);
229
+ if (metric.direction !== void 0 && metric.direction !== "higher-better" && metric.direction !== "lower-better") {
230
+ throw new Error(`display.metrics[${i}].direction must be 'higher-better' or 'lower-better'`);
231
+ }
232
+ if (metric.decimals !== void 0 && (!Number.isInteger(metric.decimals) || metric.decimals < 0)) {
233
+ throw new Error(`display.metrics[${i}].decimals must be a non-negative integer`);
234
+ }
235
+ if (metric.order !== void 0 && (!Number.isInteger(metric.order) || metric.order < 0)) {
236
+ throw new Error(`display.metrics[${i}].order must be a non-negative integer`);
111
237
  }
112
238
  }
113
239
  }
240
+ if (display.steps !== void 0) {
241
+ if (!Array.isArray(display.steps)) {
242
+ throw new Error("display.steps must be an array");
243
+ }
244
+ const seenStepKeys = /* @__PURE__ */ new Set();
245
+ for (let i = 0; i < display.steps.length; i++) {
246
+ const step = display.steps[i];
247
+ if (step === null || typeof step !== "object" || Array.isArray(step)) {
248
+ throw new Error(`display.steps[${i}] must be an object`);
249
+ }
250
+ assertOnlyAllowedKeys(step, ["key", "label", "order"], `display.steps[${i}]`);
251
+ const key = assertNonEmptyString(step.key, `display.steps[${i}].key`);
252
+ if (seenStepKeys.has(key)) {
253
+ throw new Error(`duplicate display step key: ${key}`);
254
+ }
255
+ seenStepKeys.add(key);
256
+ assertNonEmptyString(step.label, `display.steps[${i}].label`);
257
+ if (step.order !== void 0 && (!Number.isInteger(step.order) || step.order < 0)) {
258
+ throw new Error(`display.steps[${i}].order must be a non-negative integer`);
259
+ }
260
+ }
261
+ }
262
+ if (display.overview !== void 0) {
263
+ if (typeof display.overview !== "object" || display.overview === null || Array.isArray(display.overview)) {
264
+ throw new Error("display.overview must be an object");
265
+ }
266
+ assertOnlyAllowedKeys(display.overview, ["defaultMetric", "defaultLayout"], "display.overview");
267
+ const { defaultMetric, defaultLayout } = display.overview;
268
+ if (defaultMetric !== void 0) {
269
+ const metric = assertNonEmptyString(defaultMetric, "display.overview.defaultMetric");
270
+ const validDefaultMetrics = new Set(displayMetricKeys);
271
+ validDefaultMetrics.add("compositeScore");
272
+ validDefaultMetrics.add("task");
273
+ if (display.metrics !== void 0 && !validDefaultMetrics.has(metric)) {
274
+ throw new Error(`display.overview.defaultMetric '${metric}' is not declared in display.metrics and is not a known default (compositeScore, task)`);
275
+ }
276
+ }
277
+ if (defaultLayout !== void 0 && !["ranking", "cards", "chart", "leaderboard"].includes(defaultLayout)) {
278
+ throw new Error("display.overview.defaultLayout must be 'ranking', 'cards', 'chart', or 'leaderboard'");
279
+ }
280
+ }
281
+ }
282
+ function defineBenchmarkConfig(config) {
283
+ const issues = validateBenchmarkConfig(config);
284
+ if (config.scoring !== void 0) {
285
+ try {
286
+ validateBenchmarkScoringConfig(config.scoring, config.display);
287
+ } catch (error) {
288
+ issues.push({
289
+ field: "scoring",
290
+ message: error instanceof Error ? error.message : String(error)
291
+ });
292
+ }
293
+ }
294
+ if (issues.length > 0) {
295
+ throw new BenchmarkConfigError(issues);
296
+ }
114
297
  return config;
115
298
  }
116
299
  function defineTask(task) {
@@ -119,6 +302,136 @@ function defineTask(task) {
119
302
  }
120
303
  return task;
121
304
  }
305
+ function validateBenchmarkConfig(config) {
306
+ const issues = [];
307
+ if (!config.benchmarkSlug || typeof config.benchmarkSlug !== "string") {
308
+ issues.push({ field: "benchmarkSlug", message: "is required" });
309
+ }
310
+ if (!config.benchmarkName || typeof config.benchmarkName !== "string") {
311
+ issues.push({ field: "benchmarkName", message: "is required" });
312
+ }
313
+ if (!Array.isArray(config.participants) || config.participants.length === 0) {
314
+ issues.push({ field: "participants", message: "must be a non-empty array" });
315
+ } else {
316
+ const seenParticipants = /* @__PURE__ */ new Set();
317
+ for (let i = 0; i < config.participants.length; i++) {
318
+ const p = config.participants[i];
319
+ if (p === null || typeof p !== "object" || Array.isArray(p)) {
320
+ issues.push({ field: `participants[${i}]`, message: "must be an object" });
321
+ continue;
322
+ }
323
+ const participant = p;
324
+ if (typeof participant.name !== "string" || participant.name.trim() === "") {
325
+ issues.push({ field: `participants[${i}].name`, message: "must be a non-empty string" });
326
+ } else if (seenParticipants.has(participant.name)) {
327
+ issues.push({ field: `participants[${i}].name`, message: `duplicate participant name: ${participant.name}` });
328
+ } else {
329
+ seenParticipants.add(participant.name);
330
+ }
331
+ if (participant.requiredEnvVars !== void 0 && (!Array.isArray(participant.requiredEnvVars) || !participant.requiredEnvVars.every((v) => typeof v === "string"))) {
332
+ issues.push({ field: `participants[${i}].requiredEnvVars`, message: "must be an array of strings" });
333
+ }
334
+ }
335
+ }
336
+ if (config.phases !== void 0) {
337
+ if (config.iterations !== void 0) {
338
+ issues.push({ field: "iterations", message: "phases and iterations are mutually exclusive" });
339
+ }
340
+ if (!Array.isArray(config.phases) || config.phases.length === 0) {
341
+ issues.push({ field: "phases", message: "must be a non-empty array" });
342
+ } else {
343
+ const seen = /* @__PURE__ */ new Set();
344
+ for (let i = 0; i < config.phases.length; i++) {
345
+ const phase = config.phases[i];
346
+ if (phase === null || typeof phase !== "object" || Array.isArray(phase)) {
347
+ issues.push({ field: `phases[${i}]`, message: "must be an object" });
348
+ continue;
349
+ }
350
+ const phaseObj = phase;
351
+ if (typeof phaseObj.name !== "string" || phaseObj.name.trim() === "") {
352
+ issues.push({ field: `phases[${i}]`, message: "must have a non-empty string name" });
353
+ } else {
354
+ const name = phaseObj.name;
355
+ if (seen.has(name)) {
356
+ issues.push({ field: `phases['${name}']`, message: `duplicate phase name: ${name}` });
357
+ }
358
+ seen.add(name);
359
+ const iterations = phaseObj.iterations;
360
+ if (typeof iterations !== "number" || !Number.isInteger(iterations) || iterations < 1) {
361
+ issues.push({ field: `phases['${name}'].iterations`, message: `must be an integer >= 1 (got ${iterations})` });
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }
367
+ if (config.iterations !== void 0 && (!Number.isInteger(config.iterations) || config.iterations < 1)) {
368
+ issues.push({ field: "iterations", message: `must be an integer >= 1 (got ${config.iterations})` });
369
+ }
370
+ if (config.concurrency !== void 0 && (!Number.isInteger(config.concurrency) || config.concurrency < 1)) {
371
+ issues.push({ field: "concurrency", message: `must be an integer >= 1 (got ${config.concurrency})` });
372
+ }
373
+ if (config.staggerDelayMs !== void 0 && (!Number.isFinite(config.staggerDelayMs) || config.staggerDelayMs < 0)) {
374
+ issues.push({ field: "staggerDelayMs", message: `must be a number >= 0 (got ${config.staggerDelayMs})` });
375
+ }
376
+ if (config.groupBy !== void 0 && config.groupBy !== "participant" && config.groupBy !== "round") {
377
+ issues.push({ field: "groupBy", message: `must be 'participant' or 'round' (got ${config.groupBy})` });
378
+ }
379
+ if (config.shapes !== void 0) {
380
+ if (typeof config.shapes !== "object" || config.shapes === null || Array.isArray(config.shapes)) {
381
+ issues.push({ field: "shapes", message: "must be a plain object" });
382
+ } else {
383
+ for (const [shapeName, shape] of Object.entries(config.shapes)) {
384
+ if (shape === null || typeof shape !== "object" || Array.isArray(shape)) {
385
+ issues.push({ field: `shapes['${shapeName}']`, message: "must be an object" });
386
+ continue;
387
+ }
388
+ const shapeObj = shape;
389
+ const slug = shapeObj.slug;
390
+ if (typeof slug !== "string" || slug === "" || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
391
+ issues.push({ field: `shapes['${shapeName}'].slug`, message: `needs a lowercase slug (got ${JSON.stringify(slug)})` });
392
+ }
393
+ const name = shapeObj.name;
394
+ if (name !== void 0 && (typeof name !== "string" || name.trim() === "")) {
395
+ issues.push({ field: `shapes['${shapeName}'].name`, message: "must be a non-empty string" });
396
+ }
397
+ const staggerDelayMs = shapeObj.staggerDelayMs;
398
+ if (staggerDelayMs !== void 0 && (typeof staggerDelayMs !== "number" || !Number.isFinite(staggerDelayMs) || staggerDelayMs < 0)) {
399
+ issues.push({ field: `shapes['${shapeName}'].staggerDelayMs`, message: `must be a number >= 0 (got ${staggerDelayMs})` });
400
+ }
401
+ }
402
+ }
403
+ }
404
+ if (config.dimensions !== void 0) {
405
+ if (config.dimensions === null || typeof config.dimensions !== "object" || Array.isArray(config.dimensions)) {
406
+ issues.push({ field: "dimensions", message: "must be a plain object" });
407
+ }
408
+ }
409
+ if (config.customCliFlags !== void 0) {
410
+ if (!Array.isArray(config.customCliFlags) || !config.customCliFlags.every((f) => typeof f === "string" && f.startsWith("--"))) {
411
+ issues.push({ field: "customCliFlags", message: 'must be an array of strings starting with "--"' });
412
+ }
413
+ }
414
+ if (config.display !== void 0) {
415
+ try {
416
+ validateBenchmarkDisplayConfig(config.display);
417
+ } catch (error) {
418
+ issues.push({
419
+ field: "display",
420
+ message: error instanceof Error ? error.message : String(error)
421
+ });
422
+ }
423
+ }
424
+ if (config.display?.overview?.defaultMetric === "compositeScore" && config.scoring === void 0 && config.onScore === void 0) {
425
+ issues.push({
426
+ field: "display.overview.defaultMetric",
427
+ message: "cannot be 'compositeScore' without config.scoring or config.onScore"
428
+ });
429
+ }
430
+ return issues;
431
+ }
432
+ function defineOnComplete(onComplete) {
433
+ return onComplete;
434
+ }
122
435
 
123
436
  // src/no-available-participants.ts
124
437
  var NoAvailableParticipantsError = class extends Error {
@@ -135,7 +448,9 @@ var NoAvailableParticipantsError = class extends Error {
135
448
  // src/runner.ts
136
449
  var import_node_child_process = require("child_process");
137
450
  var import_node_os = __toESM(require("os"), 1);
138
- var import_client = require("@benchsdk/client");
451
+ var import_api = require("@benchsdk/api");
452
+ var import_cli = require("@benchsdk/cli");
453
+ var import_worker = require("@benchsdk/worker");
139
454
 
140
455
  // src/scoring.ts
141
456
  function isFiniteNumber(v) {
@@ -191,40 +506,121 @@ var higherIsBetter = (name, opts) => ({
191
506
  ...opts,
192
507
  higherIsBetter: true
193
508
  });
194
- function score(outcome, spec) {
509
+ var WEIGHT_SUM_TOLERANCE = 0.01;
510
+ var ScoringSpecError = class extends Error {
511
+ constructor(message) {
512
+ super(message);
513
+ this.name = "ScoringSpecError";
514
+ }
515
+ };
516
+ function validateScoringSpec(spec) {
517
+ const totalWeight = spec.metrics.reduce(
518
+ (sum, m) => sum + m.weights.median + m.weights.p95 + m.weights.p99,
519
+ 0
520
+ );
521
+ if (Math.abs(totalWeight - 1) > WEIGHT_SUM_TOLERANCE) {
522
+ const breakdown = spec.metrics.map((m) => `${m.name}=${(m.weights.median + m.weights.p95 + m.weights.p99).toFixed(3)}`).join(", ");
523
+ throw new ScoringSpecError(
524
+ `Scoring spec weights sum to ${totalWeight.toFixed(3)}, expected 1.0 (\xB1${WEIGHT_SUM_TOLERANCE}). Each metric's weights.median + weights.p95 + weights.p99, summed across every declared metric, must total 1.0 for compositeScore to stay a meaningful 0-100 scale. Per-metric totals: ${breakdown || "(no metrics declared)"}`
525
+ );
526
+ }
527
+ }
528
+ function groupRecordsByKey(records, key) {
529
+ const groups = /* @__PURE__ */ new Map();
530
+ for (const record of records) {
531
+ const raw = record.data?.[key];
532
+ const value = raw === void 0 ? void 0 : raw;
533
+ const mapKey = value === void 0 ? "__undefined__" : JSON.stringify(value);
534
+ let group = groups.get(mapKey);
535
+ if (!group) {
536
+ group = { value, records: [] };
537
+ groups.set(mapKey, group);
538
+ }
539
+ group.records.push(record);
540
+ }
541
+ return Array.from(groups.values());
542
+ }
543
+ function scoreGroup(records, spec, baseDimensions, groupKey, groupValue, provider, unitByMetric) {
195
544
  const successFilter = spec.success ?? ((r) => r.status === "success");
196
- const dimensions = toJsonObject(spec.dimensions ?? {});
545
+ const passing = records.filter(successFilter);
546
+ const successRate = records.length === 0 ? 0 : passing.length / records.length;
547
+ const skipped = records.length === 0;
548
+ let metricScoresSum = 0;
549
+ const metrics = [];
550
+ for (const metric of spec.metrics) {
551
+ const samples = collectSamples(metric, passing);
552
+ if (samples.length === 0) {
553
+ continue;
554
+ }
555
+ const { median, p95, p99 } = computeStats(samples, metric.trim ?? 0.05);
556
+ const metricScore = metric.weights.median * scoreStat(median, metric) + metric.weights.p95 * scoreStat(p95, metric) + metric.weights.p99 * scoreStat(p99, metric);
557
+ metricScoresSum += metricScore;
558
+ const unitKey = typeof metric.value === "string" ? metric.value : metric.name;
559
+ const unit = metric.unit ?? unitByMetric.get(unitKey) ?? "";
560
+ metrics.push({ name: metric.name, unit, median, p95, p99 });
561
+ }
562
+ const compositeScore = successRate === 0 ? 0 : Math.round(metricScoresSum * successRate * 100) / 100;
563
+ const dimensions = toJsonObject({
564
+ ...baseDimensions,
565
+ ...groupKey !== void 0 && groupValue !== void 0 ? { [groupKey]: groupValue } : {}
566
+ });
567
+ return {
568
+ provider,
569
+ dimensions,
570
+ metrics,
571
+ compositeScore,
572
+ successRate,
573
+ skipped
574
+ };
575
+ }
576
+ function score(outcome, spec, displayMetrics) {
577
+ validateScoringSpec(spec);
578
+ const baseDimensions = toJsonObject(spec.dimensions ?? {});
579
+ const unitByMetric = new Map(displayMetrics?.map((m) => [m.key, m.unit]));
197
580
  const results = [];
198
581
  for (const { participant, records } of outcome.participants) {
199
- const passing = records.filter(successFilter);
200
- const successRate = records.length === 0 ? 0 : passing.length / records.length;
201
- const skipped = records.length === 0;
202
- let metricScoresSum = 0;
203
- const metrics = [];
204
- for (const metric of spec.metrics) {
205
- const samples = collectSamples(metric, passing);
206
- if (samples.length === 0) {
207
- continue;
208
- }
209
- const { median, p95, p99 } = computeStats(samples, metric.trim ?? 0.05);
210
- const metricScore = metric.weights.median * scoreStat(median, metric) + metric.weights.p95 * scoreStat(p95, metric) + metric.weights.p99 * scoreStat(p99, metric);
211
- metricScoresSum += metricScore;
212
- metrics.push({ name: metric.name, unit: metric.unit, median, p95, p99 });
213
- }
214
- const compositeScore = successRate === 0 ? 0 : Math.round(metricScoresSum * successRate * 100) / 100;
215
- results.push({
216
- provider: participant,
217
- dimensions,
218
- metrics,
219
- compositeScore,
220
- successRate,
221
- skipped
222
- });
582
+ const groups = spec.groupBy && records.length > 0 ? [{ value: void 0, records }, ...groupRecordsByKey(records, spec.groupBy)] : [{ value: void 0, records }];
583
+ for (const group of groups) {
584
+ results.push(scoreGroup(group.records, spec, baseDimensions, spec.groupBy, group.value, participant, unitByMetric));
585
+ }
223
586
  }
224
587
  return results;
225
588
  }
589
+ function scoringConfigToSpec(config, dimensions, display) {
590
+ const success = config.success;
591
+ const unitByMetric = new Map(display?.metrics?.map((m) => [m.key, m.unit]));
592
+ return {
593
+ ...dimensions ? { dimensions: toJsonObject(dimensions) } : {},
594
+ ...config.groupBy ? { groupBy: config.groupBy } : {},
595
+ ...success ? {
596
+ success: (record) => record.status === "success" && Object.entries(success.requireData).every(([key, value]) => record.data?.[key] === value)
597
+ } : {},
598
+ metrics: config.metrics.map((metric) => ({
599
+ name: metric.key,
600
+ value: metric.key,
601
+ unit: (display?.metrics === void 0 ? metric.unit : unitByMetric.get(metric.key) ?? metric.unit) ?? "",
602
+ ceiling: metric.ceiling,
603
+ floor: metric.floor,
604
+ higherIsBetter: metric.higherIsBetter,
605
+ weights: metric.weights,
606
+ trim: metric.trim
607
+ }))
608
+ };
609
+ }
226
610
 
227
611
  // src/log-buffer.ts
612
+ var LOG_LEVEL_ORDER = ["debug", "info", "warn", "error"];
613
+ function isLogOptions(value) {
614
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
615
+ const o = value;
616
+ const keys = Object.keys(o);
617
+ if (keys.length === 0) return false;
618
+ if (!keys.every((k) => k === "level" || k === "meta")) return false;
619
+ if (o.level !== void 0 && (typeof o.level !== "string" || !LOG_LEVEL_ORDER.includes(o.level))) {
620
+ return false;
621
+ }
622
+ return true;
623
+ }
228
624
  var LogBuffer = class {
229
625
  lines = [];
230
626
  step(taskIndex, stepName, outcome) {
@@ -241,9 +637,15 @@ var LogBuffer = class {
241
637
  }
242
638
  }
243
639
  /** Appends a free-form narration line (backs the task context's `log`). */
244
- line(message, meta) {
245
- const suffix = meta && Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : "";
246
- this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} ${message}${suffix}`);
640
+ line(message, metaOrOptions) {
641
+ const opts = isLogOptions(metaOrOptions) ? { level: metaOrOptions.level ?? "info", meta: metaOrOptions.meta } : { level: "info", meta: metaOrOptions };
642
+ const suffix = opts.meta && Object.keys(opts.meta).length > 0 ? ` ${JSON.stringify(opts.meta)}` : "";
643
+ const taskMatch = message.match(/^(\[task \d+\])\s*(.*)$/);
644
+ if (taskMatch) {
645
+ this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} ${taskMatch[1]} [${opts.level}] ${taskMatch[2]}${suffix}`);
646
+ } else {
647
+ this.lines.push(`${(/* @__PURE__ */ new Date()).toISOString()} [${opts.level}] ${message}${suffix}`);
648
+ }
247
649
  }
248
650
  isEmpty() {
249
651
  return this.lines.length === 0;
@@ -257,7 +659,6 @@ function indent(text, prefix = "") {
257
659
  }
258
660
 
259
661
  // src/runner.ts
260
- var DEFAULT_PLATFORM_URL = "https://platform.computesdk.com";
261
662
  function isEnvNoIngest() {
262
663
  const v = process.env.BENCHSDK_NO_INGEST;
263
664
  return v === "1" || v?.toLowerCase() === "true";
@@ -265,15 +666,68 @@ function isEnvNoIngest() {
265
666
  function sleep(ms) {
266
667
  return new Promise((resolve2) => setTimeout(resolve2, ms));
267
668
  }
669
+ function runWithConcurrency(fns, limit) {
670
+ if (limit >= fns.length || fns.length === 0) {
671
+ return Promise.all(fns.map((fn) => fn()));
672
+ }
673
+ const results = new Array(fns.length);
674
+ let running = 0;
675
+ let completed = 0;
676
+ let nextIndex = 0;
677
+ return new Promise((resolve2, reject) => {
678
+ const runNext = () => {
679
+ if (completed === fns.length) {
680
+ resolve2(results);
681
+ return;
682
+ }
683
+ while (running < limit && nextIndex < fns.length) {
684
+ const index = nextIndex++;
685
+ running++;
686
+ fns[index]().then(
687
+ (value) => {
688
+ results[index] = value;
689
+ running--;
690
+ completed++;
691
+ runNext();
692
+ },
693
+ (error) => reject(error)
694
+ );
695
+ }
696
+ };
697
+ runNext();
698
+ });
699
+ }
268
700
  function getErrorCode(error) {
269
- return error instanceof Error && error.name ? error.name : "ERROR";
701
+ if (error instanceof Error && "code" in error && typeof error.code === "string" && error.code) {
702
+ return error.code;
703
+ }
704
+ if (error instanceof Error && error.name) return error.name;
705
+ return "ERROR";
706
+ }
707
+ function isTaskError(error) {
708
+ return error instanceof Error && (error instanceof TaskError || error.name === "TaskError");
270
709
  }
271
- function withTimeout(promise, ms, name) {
710
+ var STEP_OUTCOME_KEYS = /* @__PURE__ */ new Set(["stdout", "stderr", "error", "exitCode", "code", "signal", "pid"]);
711
+ function isStepOutcome(value) {
712
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
713
+ const o = value;
714
+ const keys = Object.keys(o);
715
+ if (keys.length === 0) return false;
716
+ if (!keys.every((k) => STEP_OUTCOME_KEYS.has(k))) return false;
717
+ return typeof o.stdout === "string" || typeof o.stderr === "string" || typeof o.error === "string";
718
+ }
719
+ function withTimeout(promise, { stepName, timeoutMs, participantSlug }) {
272
720
  return new Promise((resolve2, reject) => {
273
- const timer = setTimeout(
274
- () => reject(new TaskError(`Step "${name}" timed out after ${ms}ms`, { code: "step_timeout" })),
275
- ms
276
- );
721
+ const timer = setTimeout(() => {
722
+ const participant = participantSlug ? ` for participant "${participantSlug}"` : "";
723
+ reject(
724
+ new TaskError(`Step "${stepName}" timed out after ${timeoutMs}ms${participant}`, {
725
+ code: "step_timeout",
726
+ step: stepName,
727
+ timeoutMs
728
+ })
729
+ );
730
+ }, timeoutMs);
277
731
  promise.then(
278
732
  (value) => {
279
733
  clearTimeout(timer);
@@ -286,20 +740,23 @@ function withTimeout(promise, ms, name) {
286
740
  );
287
741
  });
288
742
  }
289
- async function runStepInvocations(name, fn, options) {
290
- const requestedConcurrency = options?.concurrency;
291
- if (requestedConcurrency !== void 0 && (!Number.isInteger(requestedConcurrency) || requestedConcurrency < 1)) {
292
- throw new Error(`step "${name}" concurrency must be an integer >= 1 (got ${requestedConcurrency})`);
743
+ async function runStepInvocations(name, fn, options, participantSlug) {
744
+ if (options?.concurrency !== void 0) {
745
+ console.warn(`[benchsdk] step "${name}" option "concurrency" is deprecated; use "parallelInvocations"`);
746
+ }
747
+ const requestedParallelism = options?.parallelInvocations ?? options?.concurrency;
748
+ if (requestedParallelism !== void 0 && (!Number.isInteger(requestedParallelism) || requestedParallelism < 1)) {
749
+ throw new Error(`step "${name}" parallelInvocations must be an integer >= 1 (got ${requestedParallelism})`);
293
750
  }
294
751
  const timeoutMs = options?.timeoutMs;
295
752
  if (timeoutMs !== void 0 && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
296
753
  throw new Error(`step "${name}" timeoutMs must be a number >= 0 (got ${timeoutMs})`);
297
754
  }
298
- const count = requestedConcurrency ?? 1;
755
+ const count = requestedParallelism ?? 1;
299
756
  const invocations = Array.from({ length: count }, () => {
300
757
  const promise = Promise.resolve().then(() => fn());
301
758
  if (timeoutMs === void 0) return promise;
302
- return withTimeout(promise, timeoutMs, name);
759
+ return withTimeout(promise, { stepName: name, timeoutMs, participantSlug });
303
760
  });
304
761
  if (count === 1) {
305
762
  return invocations[0];
@@ -317,18 +774,20 @@ async function runStepInvocations(name, fn, options) {
317
774
  if (firstError !== void 0) throw firstError;
318
775
  return results;
319
776
  }
320
- async function runStepWithClient(clientStep, name, fn, options) {
321
- const { concurrency: runnerConcurrency, timeoutMs, ...clientOptions } = options ?? {};
777
+ async function runStepWithClient(clientStep, name, fn, options, participantSlug) {
778
+ const { parallelInvocations: runnerParallelism, concurrency: deprecatedConcurrency, timeoutMs, ...clientOptions } = options ?? {};
322
779
  const clientStepOptions = {
323
780
  ...clientOptions,
324
781
  timeoutMs,
325
- stepConcurrency: runnerConcurrency
782
+ stepConcurrency: runnerParallelism ?? deprecatedConcurrency
326
783
  };
327
- const result = await clientStep(name, () => runStepInvocations(name, fn, options), clientStepOptions);
784
+ const result = await clientStep(name, () => runStepInvocations(name, fn, options, participantSlug), clientStepOptions);
328
785
  return result;
329
786
  }
330
- function parseCliArgs(argv) {
787
+ function parseCliArgs(argv, allowedCustomFlags) {
331
788
  const args = {};
789
+ const unknown = [];
790
+ const allowed = new Set(allowedCustomFlags ?? []);
332
791
  const readValue = (raw, i) => {
333
792
  const eq = raw.indexOf("=");
334
793
  if (eq !== -1) return { value: raw.slice(eq + 1), nextIndex: i };
@@ -420,22 +879,46 @@ function parseCliArgs(argv) {
420
879
  case "--dry-run":
421
880
  args.noIngest = true;
422
881
  break;
423
- default:
882
+ default: {
883
+ if (allowed.has(name)) {
884
+ if (!arg.includes("=")) {
885
+ const next = argv[i + 1];
886
+ if (next && !next.startsWith("-")) {
887
+ i++;
888
+ }
889
+ }
890
+ } else {
891
+ unknown.push(name);
892
+ if (!arg.includes("=")) {
893
+ const next = argv[i + 1];
894
+ if (next && !next.startsWith("-")) {
895
+ i++;
896
+ }
897
+ }
898
+ }
424
899
  break;
900
+ }
425
901
  }
426
902
  }
903
+ if (unknown.length > 0) {
904
+ throw new Error(`Unknown flag(s): ${unknown.join(", ")}`);
905
+ }
427
906
  if (!args.noIngest && isEnvNoIngest()) {
428
907
  args.noIngest = true;
429
908
  }
430
909
  return args;
431
910
  }
432
911
  function mergeConfig(config, args) {
433
- const phaseTotal = config.phases?.reduce((sum, p) => sum + p.iterations, 0);
434
- if (phaseTotal !== void 0 && args.iterations !== void 0) {
435
- console.warn("--iterations is ignored because this benchmark declares phases.");
912
+ const phases = config.phases;
913
+ const unevenPhases = phases !== void 0 && phases.some((p) => p.iterations !== phases[0].iterations);
914
+ if (unevenPhases && args.iterations !== void 0) {
915
+ console.warn("--iterations is ignored because this benchmark sizes its phases individually.");
436
916
  }
917
+ const phaseIterations = phases !== void 0 && !unevenPhases ? args.iterations : void 0;
918
+ const phaseTotal = phases !== void 0 ? phaseIterations !== void 0 ? phaseIterations * phases.length : phases.reduce((sum, p) => sum + p.iterations, 0) : void 0;
437
919
  const resolved = {
438
920
  iterations: phaseTotal ?? args.iterations ?? config.iterations ?? 1,
921
+ phaseIterations,
439
922
  concurrency: args.concurrency ?? config.concurrency ?? 1,
440
923
  staggerDelayMs: args.staggerDelayMs ?? config.staggerDelayMs ?? 0,
441
924
  groupBy: args.groupBy ?? config.groupBy ?? "participant",
@@ -449,13 +932,16 @@ function mergeConfig(config, args) {
449
932
  }
450
933
  return resolved;
451
934
  }
452
- function buildSchedule(config, iterations, task) {
935
+ function buildSchedule(config, resolved, task) {
453
936
  if (config.phases?.length) {
454
937
  return config.phases.flatMap(
455
- (phase) => Array.from({ length: phase.iterations }, () => ({ phase: phase.name, task }))
938
+ (phase) => Array.from({ length: resolved.phaseIterations ?? phase.iterations }, () => ({
939
+ phase: phase.name,
940
+ task
941
+ }))
456
942
  );
457
943
  }
458
- return Array.from({ length: iterations }, () => ({ phase: void 0, task }));
944
+ return Array.from({ length: resolved.iterations }, () => ({ phase: void 0, task }));
459
945
  }
460
946
  function defaultOnResult(record, meta) {
461
947
  const n = record.taskIndex + 1;
@@ -463,21 +949,10 @@ function defaultOnResult(record, meta) {
463
949
  const data = record.data && Object.keys(record.data).length > 0 ? ` ${JSON.stringify(record.data)}` : "";
464
950
  console.log(` [${meta.participant}] Task ${n}/${meta.iterations}: success${data}`);
465
951
  } else {
466
- console.log(` [${meta.participant}] Task ${n}/${meta.iterations}: FAILED \u2014 ${record.errorCode ?? "unknown error"}`);
467
- }
468
- }
469
- function resolvePlatform() {
470
- const root = (process.env.BENCHMARKS_PLATFORM_URL || DEFAULT_PLATFORM_URL).replace(/\/+$/, "");
471
- const apiKey = process.env.BENCHMARKS_PLATFORM_API_KEY;
472
- if (!apiKey) {
473
- throw new Error(
474
- "BENCHMARKS_PLATFORM_API_KEY is required. Create an org-scoped API key in your organization settings on the platform and set it in your .env."
475
- );
952
+ const detail = record.data?.errorMessage ?? record.data?.error;
953
+ const suffix = typeof detail === "string" && detail.length > 0 ? `: ${detail}` : "";
954
+ console.log(` [${meta.participant}] Task ${n}/${meta.iterations}: FAILED \u2014 ${record.errorCode ?? "unknown error"}${suffix}`);
476
955
  }
477
- return {
478
- baseUrl: `${root}/api/v1`,
479
- apiKey
480
- };
481
956
  }
482
957
  function resolveShape(config, shapeName) {
483
958
  if (!shapeName) return void 0;
@@ -510,30 +985,81 @@ function dashboardUrlFor(baseUrl, organizationSlug, benchmarkSlug, runId) {
510
985
  return `${baseUrl.replace(/\/api\/v1\/?$/, "")}/${organizationSlug}/benchmarks/${benchmarkSlug}/runs/${runId}`;
511
986
  }
512
987
  function resolveParticipants(config, resolved) {
513
- const { available, skipped } = (0, import_client.filterParticipantsByEnv)((0, import_client.selectParticipants)(config.participants, resolved.providers));
988
+ const { available, skipped } = (0, import_worker.filterParticipantsByEnv)((0, import_worker.selectParticipants)(config.participants, resolved.providers));
514
989
  for (const s of skipped) {
515
990
  console.log(`Skipping ${s.name}: missing ${s.missing.join(", ")}`);
516
991
  }
517
992
  if (available.length === 0) throw new NoAvailableParticipantsError(skipped);
518
993
  return available;
519
994
  }
520
- async function runBenchmark(fileConfig, task, argv = []) {
521
- const args = parseCliArgs(argv);
995
+ function resolveTriggerSource(env = process.env) {
996
+ const source = env.BENCH_TRIGGER_SOURCE?.trim();
997
+ if (source) return source;
998
+ return env.GITHUB_EVENT_NAME?.trim() || "manual";
999
+ }
1000
+ function triggerToJson(env = process.env) {
1001
+ const requestedBy = env.BENCH_TRIGGER_REQUESTED_BY?.trim();
1002
+ const requestId = env.BENCH_TRIGGER_REQUEST_ID?.trim();
1003
+ const event = env.GITHUB_EVENT_NAME?.trim();
1004
+ return {
1005
+ source: resolveTriggerSource(env),
1006
+ ...event ? { event } : {},
1007
+ ...requestedBy ? { requestedBy } : {},
1008
+ ...requestId ? { requestId } : {}
1009
+ };
1010
+ }
1011
+ function runConfigToJson(config, resolved, participants, env = process.env) {
1012
+ const phases = config.phases?.map((phase) => ({
1013
+ name: phase.name,
1014
+ iterations: resolved.phaseIterations ?? phase.iterations
1015
+ }));
1016
+ const runConfig = {
1017
+ benchmarkSlug: config.benchmarkSlug,
1018
+ benchmarkName: config.benchmarkName,
1019
+ ...resolved.phaseIterations !== void 0 ? { phaseIterations: resolved.phaseIterations } : {},
1020
+ ...phases ? { phases } : {},
1021
+ ...!config.phases ? { iterations: resolved.iterations } : {},
1022
+ concurrency: resolved.concurrency,
1023
+ staggerDelayMs: resolved.staggerDelayMs,
1024
+ groupBy: resolved.groupBy,
1025
+ ...config.dimensions ? { dimensions: config.dimensions } : {},
1026
+ ...config.scoring ? { scoring: config.scoring } : {},
1027
+ participants,
1028
+ trigger: triggerToJson(env)
1029
+ };
1030
+ return JSON.parse(JSON.stringify(runConfig));
1031
+ }
1032
+ async function runBenchmark(fileConfig, task, argv = [], options = {}) {
1033
+ const args = parseCliArgs(argv, fileConfig.customCliFlags);
522
1034
  const noIngest = args.noIngest ?? isEnvNoIngest();
523
1035
  const shaped = applyShape(fileConfig, resolveShape(fileConfig, args.shape));
524
1036
  const config = applyIdentityOverrides(shaped, args);
525
1037
  const resolved = mergeConfig(config, args);
526
- const available = resolveParticipants(config, resolved);
527
1038
  let baseUrl = "";
528
1039
  let apiKey = "";
1040
+ let token;
1041
+ let orgSlug;
1042
+ let orgId;
529
1043
  let client = null;
530
- if (!noIngest) {
531
- ({ baseUrl, apiKey } = resolvePlatform());
532
- client = (0, import_client.createBenchmarkClient)({ baseUrl, apiKey });
1044
+ const auth = noIngest ? await (0, import_cli.resolveAuth)({ baseUrl: options.baseUrl, apiKey: options.apiKey }).catch(() => null) : await (0, import_cli.resolveAuth)({ baseUrl: options.baseUrl, apiKey: options.apiKey });
1045
+ if (auth) {
1046
+ baseUrl = auth.apiBaseUrl;
1047
+ apiKey = auth.apiKey ?? "";
1048
+ token = auth.token;
1049
+ orgSlug = auth.orgSlug;
1050
+ orgId = auth.orgId;
1051
+ client = (0, import_api.createBenchmarkClient)({
1052
+ baseUrl: auth.apiBaseUrl,
1053
+ apiKey: auth.apiKey,
1054
+ token: auth.token,
1055
+ orgSlug: auth.orgSlug,
1056
+ orgId: auth.orgId
1057
+ });
533
1058
  }
534
- const schedule = buildSchedule(config, resolved.iterations, task);
1059
+ const available = resolveParticipants(config, resolved);
1060
+ const schedule = buildSchedule(config, resolved, task);
535
1061
  const totalTasks = schedule.length;
536
- const concurrencyLabel = resolved.groupBy === "round" ? "n/a (round mode)" : String(resolved.concurrency);
1062
+ const concurrencyLabel = String(resolved.concurrency);
537
1063
  console.log(`${config.benchmarkName} (self-contained)`);
538
1064
  console.log(`Date: ${(/* @__PURE__ */ new Date()).toISOString()}`);
539
1065
  if (noIngest) {
@@ -550,14 +1076,32 @@ async function runBenchmark(fileConfig, task, argv = []) {
550
1076
  runId = "no-ingest";
551
1077
  dashboardUrl = "";
552
1078
  } else {
553
- if (identityIsOurs) {
554
- await client.upsertBenchmark(config.benchmarkSlug, {
555
- name: config.benchmarkName
556
- });
1079
+ {
1080
+ const benchmarkConfig = {
1081
+ ...config.scoring ? { scoring: config.scoring } : {},
1082
+ ...config.display ? { display: config.display } : {}
1083
+ };
1084
+ let initializeTarget = identityIsOurs;
1085
+ if (!initializeTarget) {
1086
+ try {
1087
+ await client.getBenchmark(config.benchmarkSlug);
1088
+ } catch (error) {
1089
+ if (!(error instanceof import_api.BenchmarkApiError && error.status === 404)) throw error;
1090
+ initializeTarget = true;
1091
+ }
1092
+ }
1093
+ const upsertInput = {};
1094
+ if (initializeTarget) {
1095
+ upsertInput.name = config.benchmarkName;
1096
+ if (Object.keys(benchmarkConfig).length > 0) upsertInput.config = benchmarkConfig;
1097
+ }
1098
+ await client.upsertBenchmark(config.benchmarkSlug, upsertInput);
557
1099
  }
1100
+ const runConfig = client ? runConfigToJson(config, resolved, available.map((p) => p.name)) : {};
558
1101
  if (args.runKey) {
559
1102
  const { run: run2, organizationSlug } = await client.createRun(config.benchmarkSlug, {
560
- runKey: args.runKey
1103
+ runKey: args.runKey,
1104
+ config: runConfig
561
1105
  });
562
1106
  runId = run2.id;
563
1107
  dashboardUrl = dashboardUrlFor(baseUrl, organizationSlug, config.benchmarkSlug, run2.id);
@@ -571,7 +1115,8 @@ async function runBenchmark(fileConfig, task, argv = []) {
571
1115
  const { run: run2, organizationSlug } = await client.createRun(config.benchmarkSlug, {
572
1116
  totalTasks,
573
1117
  workerCount: 1,
574
- participants: available.map((p) => p.name)
1118
+ participants: available.map((p) => p.name),
1119
+ config: runConfig
575
1120
  });
576
1121
  runId = run2.id;
577
1122
  dashboardUrl = dashboardUrlFor(baseUrl, organizationSlug, config.benchmarkSlug, run2.id);
@@ -583,9 +1128,9 @@ async function runBenchmark(fileConfig, task, argv = []) {
583
1128
  const onResult = defaultOnResult;
584
1129
  let participantRecords;
585
1130
  if (resolved.groupBy === "round") {
586
- participantRecords = await runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, onResult, noIngest);
1131
+ participantRecords = await runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, token, orgSlug, orgId, onResult, noIngest);
587
1132
  } else {
588
- participantRecords = await runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult);
1133
+ participantRecords = await runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult, noIngest);
589
1134
  }
590
1135
  console.log(`All done. ${noIngest ? "No platform run created." : `View at: ${dashboardUrl}`}`);
591
1136
  const outcome = {
@@ -594,20 +1139,25 @@ async function runBenchmark(fileConfig, task, argv = []) {
594
1139
  participants: participantRecords,
595
1140
  config: resolved
596
1141
  };
597
- if (client && config.onScore) {
1142
+ if (!noIngest && client && (config.onScore || config.scoring)) {
598
1143
  try {
599
- const spec = await config.onScore(lowerIsBetter, higherIsBetter);
600
- const scored = score(outcome, spec);
1144
+ const spec = config.onScore ? await config.onScore(lowerIsBetter, higherIsBetter) : scoringConfigToSpec(config.scoring, config.dimensions, config.display);
1145
+ const scored = score(outcome, spec, config.display?.metrics);
601
1146
  const run2 = {
602
1147
  gitSha: process.env.GITHUB_SHA ?? getGitSha(),
603
1148
  gitRef: process.env.GITHUB_REF_NAME ?? process.env.GITHUB_REF ?? getGitRef(),
604
- triggeredBy: process.env.GITHUB_EVENT_NAME ?? "manual",
1149
+ triggeredBy: resolveTriggerSource(),
605
1150
  nodeVersion: process.version,
606
1151
  platform: import_node_os.default.platform(),
607
1152
  arch: import_node_os.default.arch()
608
1153
  };
609
- await client.submitRunSummary(config.benchmarkSlug, runId, { run: run2, results: scored });
1154
+ await client.submitRunSummary(config.benchmarkSlug, runId, {
1155
+ run: run2,
1156
+ results: scored,
1157
+ ...config.scoring ? { scoring: config.scoring } : {}
1158
+ });
610
1159
  } catch (err) {
1160
+ if (err instanceof ScoringSpecError) throw err;
611
1161
  const message = err instanceof Error ? err.message : String(err);
612
1162
  console.warn(`[benchsdk-runner] failed to submit run summary: ${message}`);
613
1163
  }
@@ -615,6 +1165,22 @@ async function runBenchmark(fileConfig, task, argv = []) {
615
1165
  if (config.onComplete) await config.onComplete(outcome);
616
1166
  return outcome;
617
1167
  }
1168
+ async function runBenchmarkWorker(options) {
1169
+ const config = defineBenchmarkConfig({
1170
+ benchmarkSlug: options.benchmarkSlug,
1171
+ benchmarkName: options.benchmarkName ?? options.benchmarkSlug,
1172
+ participants: [options.participant],
1173
+ iterations: options.iterations ?? 1,
1174
+ concurrency: options.concurrency ?? 1,
1175
+ staggerDelayMs: options.staggerDelayMs ?? 0,
1176
+ groupBy: options.groupBy ?? "participant",
1177
+ defaultProviders: [options.participant.name]
1178
+ });
1179
+ const argv = [];
1180
+ if (options.runKey) argv.push("--run-key", options.runKey);
1181
+ if (options.noIngest) argv.push("--dry-run");
1182
+ return runBenchmark(config, options.task, argv);
1183
+ }
618
1184
  function getGitSha() {
619
1185
  if (process.env.GITHUB_SHA) return process.env.GITHUB_SHA;
620
1186
  try {
@@ -632,13 +1198,13 @@ function getGitRef() {
632
1198
  return void 0;
633
1199
  }
634
1200
  }
635
- async function runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult) {
1201
+ async function runGroupedByParticipant(config, schedule, available, resolved, client, runId, onResult, noIngest) {
636
1202
  const participantRecords = [];
637
1203
  for (const participant of available) {
638
1204
  console.log(`${"=".repeat(70)}`);
639
1205
  console.log(` Participant: ${participant.name}`);
640
1206
  console.log("=".repeat(70));
641
- if (!client) {
1207
+ if (noIngest || !client) {
642
1208
  const records = [];
643
1209
  let rampStartMs2;
644
1210
  let nextIndex = 0;
@@ -670,7 +1236,7 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
670
1236
  }
671
1237
  let rampStartMs;
672
1238
  await client.planWorkers(config.benchmarkSlug, runId, participant.name);
673
- const result = await client.runWorker({
1239
+ const result = await (0, import_worker.runWorker)(client, {
674
1240
  benchmarkSlug: config.benchmarkSlug,
675
1241
  runId,
676
1242
  participantSlug: participant.name,
@@ -683,16 +1249,21 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
683
1249
  if (waitMs > 0) await sleep(waitMs);
684
1250
  }
685
1251
  const slot = schedule[scheduleIndex];
686
- const taskResult = await slot.task({
687
- participant,
688
- taskIndex: scheduleIndex,
689
- phase: slot.phase,
690
- step: (name, fn, options) => runStepWithClient(ctx.step, name, fn, options),
691
- measure: ctx.measure,
692
- log: ctx.log
693
- });
694
1252
  if (slot.phase) ctx.measure({ phase: slot.phase });
695
- return taskResult?.data;
1253
+ try {
1254
+ const taskResult = await slot.task({
1255
+ participant,
1256
+ taskIndex: scheduleIndex,
1257
+ phase: slot.phase,
1258
+ step: (name, fn, options) => runStepWithClient(ctx.step, name, fn, options, participant.name),
1259
+ measure: ctx.measure,
1260
+ log: ctx.log
1261
+ });
1262
+ return taskResult?.data;
1263
+ } catch (error) {
1264
+ if (isTaskError(error) && error.data) ctx.measure(error.data);
1265
+ throw error;
1266
+ }
696
1267
  },
697
1268
  onResult: (record) => onResult(record, { iterations: schedule.length, participant: participant.name })
698
1269
  });
@@ -708,14 +1279,17 @@ async function runGroupedByParticipant(config, schedule, available, resolved, cl
708
1279
  }
709
1280
  return participantRecords;
710
1281
  }
711
- async function runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, onResult, noIngest = false) {
1282
+ async function runGroupedByRound(config, schedule, available, resolved, client, runId, baseUrl, apiKey, token, orgSlug, orgId, onResult, noIngest = false) {
712
1283
  const reporters = /* @__PURE__ */ new Map();
713
1284
  const logBuffers = /* @__PURE__ */ new Map();
714
1285
  const failed = /* @__PURE__ */ new Map();
715
1286
  const recordsByParticipant = /* @__PURE__ */ new Map();
1287
+ let metricsCollector;
1288
+ const metricsSamples = [];
716
1289
  for (const participant of available) {
717
1290
  logBuffers.set(participant.name, new LogBuffer());
718
1291
  failed.set(participant.name, false);
1292
+ recordsByParticipant.set(participant.name, []);
719
1293
  if (noIngest || !client) {
720
1294
  reporters.set(participant.name, null);
721
1295
  continue;
@@ -726,9 +1300,12 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
726
1300
  });
727
1301
  let reporter = null;
728
1302
  try {
729
- reporter = await import_client.BenchmarkReporter.claim({
1303
+ reporter = await import_worker.BenchmarkReporter.claim({
730
1304
  baseUrl,
731
1305
  apiKey,
1306
+ token,
1307
+ orgSlug,
1308
+ orgId,
732
1309
  benchmarkSlug: config.benchmarkSlug,
733
1310
  runId,
734
1311
  participantSlug: participant.name,
@@ -742,6 +1319,10 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
742
1319
  console.warn(` ${participant.name}: could not claim a platform worker \u2014 running without platform reporting.`);
743
1320
  }
744
1321
  reporters.set(participant.name, reporter);
1322
+ if (reporter && !metricsCollector) {
1323
+ metricsCollector = (0, import_worker.createSystemMetricsCollector)();
1324
+ metricsSamples.push(await metricsCollector.sample());
1325
+ }
745
1326
  }
746
1327
  console.log(`Interleaving ${available.length} participant(s), ${schedule.length} round(s) each.
747
1328
  `);
@@ -750,7 +1331,7 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
750
1331
  if (resolved.staggerDelayMs > 0 && i > 0) {
751
1332
  await sleep(resolved.staggerDelayMs);
752
1333
  }
753
- for (const participant of available) {
1334
+ const roundFns = available.map((participant) => async () => {
754
1335
  const reporter = reporters.get(participant.name) ?? null;
755
1336
  const logBuffer = logBuffers.get(participant.name);
756
1337
  const record = await runTaskRecord(
@@ -764,9 +1345,6 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
764
1345
  if (record.status !== "success") failed.set(participant.name, true);
765
1346
  onResult(record, { iterations: schedule.length, participant: participant.name });
766
1347
  reporter?.recordResult(record);
767
- if (!recordsByParticipant.has(participant.name)) {
768
- recordsByParticipant.set(participant.name, []);
769
- }
770
1348
  const participantRecords = recordsByParticipant.get(participant.name);
771
1349
  participantRecords.push(record);
772
1350
  if (reporter) {
@@ -778,7 +1356,22 @@ async function runGroupedByRound(config, schedule, available, resolved, client,
778
1356
  });
779
1357
  await reporter.heartbeat();
780
1358
  }
781
- }
1359
+ });
1360
+ await runWithConcurrency(roundFns, resolved.concurrency);
1361
+ if (metricsCollector) metricsSamples.push(await metricsCollector.sample());
1362
+ }
1363
+ if (metricsCollector) metricsSamples.push(await metricsCollector.sample());
1364
+ metricsCollector?.stop();
1365
+ const metricsReporter = available.map((p) => reporters.get(p.name)).find((r) => Boolean(r));
1366
+ if (metricsReporter && metricsSamples.length > 0) {
1367
+ await metricsReporter.uploadArtifact({
1368
+ kind: "system-metrics",
1369
+ contentType: "application/x-ndjson",
1370
+ name: "metrics.jsonl",
1371
+ metadata: { scope: "shared-process", participants: available.map((p) => p.name) },
1372
+ body: metricsSamples.map((sample) => JSON.stringify(sample)).join("\n") + "\n"
1373
+ }).catch(() => {
1374
+ });
782
1375
  }
783
1376
  for (const participant of available) {
784
1377
  const reporter = reporters.get(participant.name) ?? null;
@@ -819,17 +1412,19 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
819
1412
  completedAt: new Date(stepStartedAtMs).toISOString(),
820
1413
  latencyMs: 0
821
1414
  };
822
- if (options?.concurrency !== void 0) stepRecord.concurrency = options.concurrency;
1415
+ const requestedParallelism = options?.parallelInvocations ?? options?.concurrency;
1416
+ if (requestedParallelism !== void 0) stepRecord.concurrency = requestedParallelism;
823
1417
  if (options?.timeoutMs !== void 0) stepRecord.timeoutMs = options.timeoutMs;
824
1418
  const previousStep = activeStep;
825
1419
  activeStep = stepRecord;
826
1420
  try {
827
- const result2 = await runStepInvocations(name, fn, options);
828
- logBuffer.step(taskIndex, name, {});
1421
+ const result2 = await runStepInvocations(name, fn, options, participant.name);
1422
+ const outcome = options?.captureOutput !== false && !Array.isArray(result2) && isStepOutcome(result2) ? result2 : {};
1423
+ logBuffer.step(taskIndex, name, outcome);
829
1424
  return result2;
830
1425
  } catch (error) {
831
1426
  stepRecord.status = "error";
832
- stepRecord.errorCode = error instanceof TaskError ? error.code ?? error.name : getErrorCode(error);
1427
+ stepRecord.errorCode = isTaskError(error) ? error.code ?? error.name : getErrorCode(error);
833
1428
  logBuffer.step(taskIndex, name, { error: error instanceof Error ? error.message : String(error) });
834
1429
  throw error;
835
1430
  } finally {
@@ -846,8 +1441,8 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
846
1441
  Object.assign(taskMeasures, data);
847
1442
  }
848
1443
  },
849
- log(message, meta) {
850
- logBuffer.line(`[task ${taskIndex}] ${message}`, meta);
1444
+ log(message, metaOrOptions) {
1445
+ logBuffer.line(`[task ${taskIndex}] ${message}`, metaOrOptions);
851
1446
  }
852
1447
  };
853
1448
  let result = void 0;
@@ -856,7 +1451,7 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
856
1451
  record.data = mergeData({ ...taskMeasures, ...result?.data ?? {} }, phase);
857
1452
  } catch (error) {
858
1453
  record.status = "error";
859
- if (error instanceof TaskError) {
1454
+ if (isTaskError(error)) {
860
1455
  record.errorCode = error.code ?? error.name;
861
1456
  record.data = mergeData({ ...taskMeasures, ...error.data ?? {} }, phase);
862
1457
  if (error.steps?.length) frameworkSteps.push(...error.steps);
@@ -892,16 +1487,145 @@ async function runTaskRecord(task, participant, scheduleIndex, taskIndex, phase,
892
1487
  // src/cli.ts
893
1488
  var import_node_path = require("path");
894
1489
  var import_node_url = require("url");
895
- var USAGE = 'Usage:\n bench run <file.bench.ts> [--shape name] [--provider a,b] [--run-key key]\n [--benchmark slug] [--name "My benchmark"]\n [--iterations N] [--concurrency N] [--stagger-delay-ms N] [--group-by participant|round]\n [--no-ingest | --dry-run]';
1490
+ var import_cli2 = require("@benchsdk/cli");
1491
+ var import_api2 = require("@benchsdk/api");
1492
+ var import_worker2 = require("@benchsdk/worker");
1493
+ var USAGE = 'Usage:\n bench run <file.bench.ts> [--shape name] [--provider a,b] [--run-key key]\n [--benchmark slug] [--name "My benchmark"]\n [--iterations N] [--concurrency N] [--stagger-delay-ms N] [--group-by participant|round]\n [--no-ingest | --dry-run] [--check]\n bench check <file.bench.ts> [--base-url <url>] [--api-key <key>]';
896
1494
  function isBenchmarkConfig(value) {
897
1495
  if (typeof value !== "object" || value === null) return false;
898
1496
  const candidate = value;
899
1497
  return typeof candidate.benchmarkSlug === "string" && Array.isArray(candidate.participants);
900
1498
  }
1499
+ function shiftFlag(argv, name) {
1500
+ const prefix = `--${name}`;
1501
+ const prefixEq = `${prefix}=`;
1502
+ const result = [];
1503
+ let value;
1504
+ for (let i = 0; i < argv.length; i++) {
1505
+ const arg = argv[i];
1506
+ if (arg === prefix) {
1507
+ const next = argv[++i];
1508
+ if (!next || next.startsWith("--")) throw new Error(USAGE);
1509
+ value = next;
1510
+ continue;
1511
+ }
1512
+ if (arg.startsWith(prefixEq)) {
1513
+ const eqValue = arg.slice(prefixEq.length);
1514
+ if (!eqValue) throw new Error(USAGE);
1515
+ value = eqValue;
1516
+ continue;
1517
+ }
1518
+ result.push(arg);
1519
+ }
1520
+ return { value, argv: result };
1521
+ }
1522
+ function shiftPlatformFlags(flags) {
1523
+ const { value: baseUrl, argv: withoutBaseUrl } = shiftFlag(flags, "base-url");
1524
+ const { value: apiKey, argv: rest } = shiftFlag(withoutBaseUrl, "api-key");
1525
+ return { baseUrl, apiKey, flags: rest };
1526
+ }
1527
+ async function runCheck(argv) {
1528
+ const [command, ...rest] = argv;
1529
+ const [file, ...flags] = rest;
1530
+ if (command !== "check" || !file || file.startsWith("-")) throw new Error(USAGE);
1531
+ const mod = await import((0, import_node_url.pathToFileURL)((0, import_node_path.resolve)(process.cwd(), file)).href);
1532
+ const config = mod.config;
1533
+ const task = mod.task ?? mod.default;
1534
+ if (!isBenchmarkConfig(config)) {
1535
+ throw new Error(`${file} must export a \`config\` created with defineBenchmarkConfig (with participants).`);
1536
+ }
1537
+ if (typeof task !== "function") {
1538
+ throw new Error(`${file} must export a \`task\` created with defineTask.`);
1539
+ }
1540
+ const cfg = config;
1541
+ const configIssues = validateBenchmarkConfig(cfg);
1542
+ if (configIssues.length > 0) {
1543
+ throw new BenchmarkConfigError(configIssues);
1544
+ }
1545
+ const { baseUrl, apiKey, flags: runnerFlags } = shiftPlatformFlags(flags);
1546
+ const parsed = parseCliArgs(runnerFlags, cfg.customCliFlags ?? []);
1547
+ resolveShape(cfg, parsed.shape);
1548
+ const dryRun = parsed.noIngest ?? false;
1549
+ let client;
1550
+ let apiOk = dryRun;
1551
+ let auth = null;
1552
+ if (!dryRun) {
1553
+ try {
1554
+ auth = await (0, import_cli2.resolveAuth)({ baseUrl, apiKey });
1555
+ client = (0, import_api2.createBenchmarkClient)({
1556
+ baseUrl: auth.apiBaseUrl,
1557
+ apiKey: auth.apiKey,
1558
+ token: auth.token,
1559
+ orgSlug: auth.orgSlug,
1560
+ orgId: auth.orgId
1561
+ });
1562
+ await client.listBenchmarks({ limit: 1 });
1563
+ apiOk = true;
1564
+ } catch (err) {
1565
+ const message = err instanceof Error ? err.message : String(err);
1566
+ console.warn(`[benchsdk] API connectivity check failed: ${message}`);
1567
+ }
1568
+ }
1569
+ const effectiveProviderNames = parsed.providers ?? cfg.defaultProviders;
1570
+ let selected;
1571
+ try {
1572
+ selected = (0, import_worker2.selectParticipants)(cfg.participants, effectiveProviderNames);
1573
+ } catch (err) {
1574
+ throw new Error(`Participant selection failed: ${err instanceof Error ? err.message : err}`);
1575
+ }
1576
+ const { available, skipped } = (0, import_worker2.filterParticipantsByEnv)(selected);
1577
+ let scoringOk = true;
1578
+ if (cfg.onScore) {
1579
+ try {
1580
+ const spec = await cfg.onScore(lowerIsBetter, higherIsBetter);
1581
+ validateScoringSpec(spec);
1582
+ } catch (err) {
1583
+ scoringOk = false;
1584
+ const message = err instanceof Error ? err.message : String(err);
1585
+ console.warn(`[benchsdk] Scoring validation failed: ${message}`);
1586
+ }
1587
+ } else if (cfg.scoring) {
1588
+ try {
1589
+ const spec = scoringConfigToSpec(cfg.scoring, cfg.dimensions);
1590
+ validateScoringSpec(spec);
1591
+ } catch (err) {
1592
+ scoringOk = false;
1593
+ const message = err instanceof Error ? err.message : String(err);
1594
+ console.warn(`[benchsdk] Scoring validation failed: ${message}`);
1595
+ }
1596
+ }
1597
+ const missingPlatformAuth = dryRun ? [] : auth ? [] : [["BENCHMARKS_PLATFORM_API_KEY or BENCHMARKS_PLATFORM_TOKEN", void 0]];
1598
+ for (const [name] of missingPlatformAuth) {
1599
+ console.warn(`[benchsdk] ${name} is not set`);
1600
+ }
1601
+ const report = {
1602
+ file,
1603
+ benchmarkSlug: cfg.benchmarkSlug,
1604
+ apiOk,
1605
+ envOk: dryRun || missingPlatformAuth.length === 0,
1606
+ participants: {
1607
+ requested: selected.map((p) => p.name),
1608
+ available: available.map((p) => p.name),
1609
+ skipped: skipped.map((s) => ({ name: s.name, missing: s.missing }))
1610
+ },
1611
+ scoringOk: cfg.scoring || cfg.onScore ? scoringOk : void 0
1612
+ };
1613
+ console.log(JSON.stringify(report, null, 2));
1614
+ const authFailure = !dryRun && missingPlatformAuth.length > 0;
1615
+ if (!apiOk || available.length === 0 || scoringOk === false || authFailure) {
1616
+ throw new Error("Benchmark check failed. See warnings above for details.");
1617
+ }
1618
+ }
901
1619
  async function runBenchmarkFile(argv) {
902
1620
  const [command, ...rest] = argv;
903
1621
  const [file, ...flags] = rest;
904
1622
  if (command !== "run" || !file || file.startsWith("-")) throw new Error(USAGE);
1623
+ const check = flags.includes("--check") || flags.includes("--validate");
1624
+ if (check) {
1625
+ const checkFlags = flags.filter((f) => f !== "--check" && f !== "--validate");
1626
+ return runCheck(["check", file, ...checkFlags]);
1627
+ }
1628
+ const { baseUrl, apiKey, flags: runnerFlags } = shiftPlatformFlags(flags);
905
1629
  const mod = await import((0, import_node_url.pathToFileURL)((0, import_node_path.resolve)(process.cwd(), file)).href);
906
1630
  const config = mod.config;
907
1631
  const task = mod.task ?? mod.default;
@@ -911,34 +1635,78 @@ async function runBenchmarkFile(argv) {
911
1635
  if (typeof task !== "function") {
912
1636
  throw new Error(`${file} must export a \`task\` created with defineTask.`);
913
1637
  }
914
- await runBenchmark(config, task, flags);
1638
+ const envFlags = [];
1639
+ if (process.env.BENCHMARK_SLUG) {
1640
+ envFlags.push("--benchmark", process.env.BENCHMARK_SLUG);
1641
+ }
1642
+ if (process.env.BENCHMARK_NAME) {
1643
+ envFlags.push("--name", process.env.BENCHMARK_NAME);
1644
+ }
1645
+ await runBenchmark(
1646
+ config,
1647
+ task,
1648
+ [...envFlags, ...runnerFlags],
1649
+ { baseUrl, apiKey }
1650
+ );
915
1651
  }
916
1652
  async function run(argv) {
1653
+ const [command, ...rest] = argv;
917
1654
  try {
918
- await runBenchmarkFile(argv);
1655
+ if (command === "run") {
1656
+ await runBenchmarkFile(argv);
1657
+ } else if (command === "check") {
1658
+ await runCheck(argv);
1659
+ } else {
1660
+ return (0, import_cli2.run)(argv);
1661
+ }
919
1662
  process.exit(0);
920
1663
  } catch (err) {
921
1664
  if (err instanceof NoAvailableParticipantsError) {
922
1665
  console.log(err.message);
923
1666
  process.exit(0);
924
1667
  }
925
- console.error("Benchmark failed:", err instanceof Error ? err.message : err);
1668
+ console.error("Benchmark failed:", String(err));
926
1669
  process.exit(1);
927
1670
  }
928
1671
  }
1672
+
1673
+ // src/index.ts
1674
+ var import_cli4 = require("@benchsdk/cli");
1675
+ var import_worker3 = require("@benchsdk/worker");
1676
+ var import_api3 = require("@benchsdk/api");
1677
+ var BENCHSDK_RUNNER_VERSION = "0.5.2";
929
1678
  // Annotate the CommonJS export names for ESM import in node:
930
1679
  0 && (module.exports = {
1680
+ AuthError,
1681
+ BENCHSDK_RUNNER_VERSION,
1682
+ BenchmarkApiError,
1683
+ BenchmarkConfigError,
1684
+ BenchmarkReporter,
931
1685
  NoAvailableParticipantsError,
1686
+ ScoringSpecError,
932
1687
  TaskError,
1688
+ claimBenchmarkReporter,
1689
+ createApiClient,
1690
+ createBenchmarkClient,
1691
+ createSystemMetricsCollector,
933
1692
  defineBenchmarkConfig,
1693
+ defineOnComplete,
934
1694
  defineTask,
1695
+ filterParticipantsByEnv,
935
1696
  higherIsBetter,
936
1697
  lowerIsBetter,
937
1698
  mergeConfig,
938
1699
  parseCliArgs,
1700
+ resolveAuth,
939
1701
  run,
940
1702
  runBenchmark,
941
1703
  runBenchmarkFile,
942
- score
1704
+ runBenchmarkWorker,
1705
+ runWorker,
1706
+ score,
1707
+ scoringConfigToSpec,
1708
+ selectParticipants,
1709
+ validateBenchmarkConfig,
1710
+ validateScoringSpec
943
1711
  });
944
1712
  //# sourceMappingURL=index.cjs.map