@omni-oss/task-bench 0.0.0-beta.1

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.
Files changed (73) hide show
  1. package/README.md +328 -0
  2. package/dist/bench/index.d.ts +82 -0
  3. package/dist/bench/index.d.ts.map +1 -0
  4. package/dist/bench/install.d.ts +5 -0
  5. package/dist/bench/install.d.ts.map +1 -0
  6. package/dist/bench/report.d.ts +9 -0
  7. package/dist/bench/report.d.ts.map +1 -0
  8. package/dist/bench/stats.d.ts +12 -0
  9. package/dist/bench/stats.d.ts.map +1 -0
  10. package/dist/cli/index.d.ts +3 -0
  11. package/dist/cli/index.d.ts.map +1 -0
  12. package/dist/config.d.ts +91 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/generate/index.d.ts +19 -0
  15. package/dist/generate/index.d.ts.map +1 -0
  16. package/dist/generate/templates.d.ts +20 -0
  17. package/dist/generate/templates.d.ts.map +1 -0
  18. package/dist/graph.d.ts +21 -0
  19. package/dist/graph.d.ts.map +1 -0
  20. package/dist/index.d.ts +16 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.mjs +2 -0
  23. package/dist/src-D3XyMXAu.mjs +1167 -0
  24. package/dist/suite/index.d.ts +49 -0
  25. package/dist/suite/index.d.ts.map +1 -0
  26. package/dist/suite/preset.d.ts +93 -0
  27. package/dist/suite/preset.d.ts.map +1 -0
  28. package/dist/suite/report.d.ts +7 -0
  29. package/dist/suite/report.d.ts.map +1 -0
  30. package/dist/task-bench-cli.mjs +107 -0
  31. package/dist/tools/index.d.ts +18 -0
  32. package/dist/tools/index.d.ts.map +1 -0
  33. package/dist/tools/moon.d.ts +10 -0
  34. package/dist/tools/moon.d.ts.map +1 -0
  35. package/dist/tools/nx.d.ts +7 -0
  36. package/dist/tools/nx.d.ts.map +1 -0
  37. package/dist/tools/omni.d.ts +7 -0
  38. package/dist/tools/omni.d.ts.map +1 -0
  39. package/dist/tools/turbo.d.ts +5 -0
  40. package/dist/tools/turbo.d.ts.map +1 -0
  41. package/dist/tools/types.d.ts +77 -0
  42. package/dist/tools/types.d.ts.map +1 -0
  43. package/package.json +41 -0
  44. package/project.omni.yaml +33 -0
  45. package/src/bench/index.ts +323 -0
  46. package/src/bench/install.ts +12 -0
  47. package/src/bench/report.ts +142 -0
  48. package/src/bench/stats.spec.ts +35 -0
  49. package/src/bench/stats.ts +38 -0
  50. package/src/cli/index.ts +410 -0
  51. package/src/config.ts +138 -0
  52. package/src/generate/index.ts +215 -0
  53. package/src/generate/templates.ts +87 -0
  54. package/src/graph.spec.ts +119 -0
  55. package/src/graph.ts +120 -0
  56. package/src/index.ts +31 -0
  57. package/src/suite/index.ts +113 -0
  58. package/src/suite/preset.spec.ts +95 -0
  59. package/src/suite/preset.ts +253 -0
  60. package/src/suite/report.ts +135 -0
  61. package/src/tools/adapters.spec.ts +95 -0
  62. package/src/tools/config.spec.ts +73 -0
  63. package/src/tools/index.ts +76 -0
  64. package/src/tools/moon.ts +106 -0
  65. package/src/tools/nx.ts +106 -0
  66. package/src/tools/omni.ts +96 -0
  67. package/src/tools/turbo.ts +78 -0
  68. package/src/tools/types.ts +116 -0
  69. package/tsconfig.json +4 -0
  70. package/tsconfig.project.json +6 -0
  71. package/tsconfig.types.json +4 -0
  72. package/vite.config.ts +29 -0
  73. package/vitest.config.unit.ts +13 -0
@@ -0,0 +1,410 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import {
6
+ Command,
7
+ Option,
8
+ type OptionValues,
9
+ } from "@commander-js/extra-typings";
10
+ import { description, name, version } from "../../package.json";
11
+ import {
12
+ type BenchEvent,
13
+ type BenchmarkResult,
14
+ DEPENDENCY_STRATEGIES,
15
+ formatMs,
16
+ formatReport,
17
+ formatSuiteMarkdown,
18
+ generateWorkspace,
19
+ getPreset,
20
+ type HarnessConfigInput,
21
+ installWorkspace,
22
+ listPresets,
23
+ parseSuite,
24
+ resolveConfig,
25
+ runBenchmark,
26
+ runSuite,
27
+ type SuiteEvent,
28
+ type SuiteResult,
29
+ TOOLS,
30
+ type Tool,
31
+ } from "..";
32
+
33
+ const program = new Command();
34
+ program.name(name).version(version).description(description);
35
+
36
+ const int = (raw: string): number => {
37
+ const n = Number.parseInt(raw, 10);
38
+ if (Number.isNaN(n)) throw new Error(`expected an integer, got "${raw}"`);
39
+ return n;
40
+ };
41
+ const float = (raw: string): number => {
42
+ const n = Number.parseFloat(raw);
43
+ if (Number.isNaN(n)) throw new Error(`expected a number, got "${raw}"`);
44
+ return n;
45
+ };
46
+ const toolList = (raw: string): Tool[] =>
47
+ raw.split(",").map((s) => {
48
+ const t = s.trim();
49
+ if (!(TOOLS as readonly string[]).includes(t)) {
50
+ throw new Error(
51
+ `unknown tool "${t}" (allowed: ${TOOLS.join(", ")})`,
52
+ );
53
+ }
54
+ return t as Tool;
55
+ });
56
+
57
+ interface GenerateOpts {
58
+ projects?: number;
59
+ tasks?: number;
60
+ strategy?: string;
61
+ layers?: number;
62
+ fanout?: number;
63
+ edgeProbability?: number;
64
+ logLines?: number;
65
+ work?: number;
66
+ outputFiles?: number;
67
+ seed?: number;
68
+ tools?: Tool[];
69
+ chain?: boolean;
70
+ fanUpstream?: boolean;
71
+ turboVersion?: string;
72
+ nxVersion?: string;
73
+ moonVersion?: string;
74
+ bunVersion?: string;
75
+ config?: string;
76
+ }
77
+
78
+ /** Merge a base config file (if any) with CLI overrides into a config input. */
79
+ function buildInput(opts: GenerateOpts): HarnessConfigInput {
80
+ const base: HarnessConfigInput = opts.config
81
+ ? JSON.parse(readFileSync(opts.config, "utf8"))
82
+ : {};
83
+
84
+ const input: HarnessConfigInput = { ...base };
85
+ if (opts.seed !== undefined) input.seed = opts.seed;
86
+ if (opts.projects !== undefined) input.projects = opts.projects;
87
+ if (opts.tasks !== undefined) input.tasksPerProject = opts.tasks;
88
+ if (opts.tools !== undefined) input.tools = opts.tools;
89
+
90
+ const dependency: NonNullable<HarnessConfigInput["dependency"]> = {
91
+ ...(base.dependency ?? {}),
92
+ };
93
+ if (opts.strategy !== undefined)
94
+ dependency.strategy = opts.strategy as NonNullable<
95
+ typeof dependency.strategy
96
+ >;
97
+ if (opts.layers !== undefined) dependency.layers = opts.layers;
98
+ if (opts.fanout !== undefined) dependency.fanout = opts.fanout;
99
+ if (opts.edgeProbability !== undefined)
100
+ dependency.edgeProbability = opts.edgeProbability;
101
+ if (Object.keys(dependency).length) input.dependency = dependency;
102
+
103
+ const task: NonNullable<HarnessConfigInput["task"]> = {
104
+ ...(base.task ?? {}),
105
+ };
106
+ if (opts.logLines !== undefined) task.logLines = opts.logLines;
107
+ if (opts.work !== undefined) task.workIterations = opts.work;
108
+ if (opts.outputFiles !== undefined) task.outputFiles = opts.outputFiles;
109
+ if (opts.chain !== undefined) task.chainWithinProject = opts.chain;
110
+ if (opts.fanUpstream !== undefined) task.fanUpstream = opts.fanUpstream;
111
+ if (Object.keys(task).length) input.task = task;
112
+
113
+ const versions: NonNullable<HarnessConfigInput["versions"]> = {
114
+ ...(base.versions ?? {}),
115
+ };
116
+ if (opts.turboVersion !== undefined) versions.turbo = opts.turboVersion;
117
+ if (opts.nxVersion !== undefined) versions.nx = opts.nxVersion;
118
+ if (opts.moonVersion !== undefined) versions.moon = opts.moonVersion;
119
+ if (opts.bunVersion !== undefined) versions.bun = opts.bunVersion;
120
+ if (Object.keys(versions).length) input.versions = versions;
121
+
122
+ return input;
123
+ }
124
+
125
+ function addGenerateOptions<
126
+ Args extends unknown[],
127
+ Opts extends OptionValues,
128
+ Global extends OptionValues,
129
+ >(cmd: Command<Args, Opts, Global>) {
130
+ return cmd
131
+ .option("--config <file>", "Base config JSON file to extend.")
132
+ .option("--seed <n>", "Deterministic graph seed.", int)
133
+ .option("--projects <n>", "Number of projects.", int)
134
+ .option("--tasks <n>", "Tasks per project.", int)
135
+ .addOption(
136
+ new Option(
137
+ "--strategy <name>",
138
+ "Dependency graph strategy.",
139
+ ).choices(DEPENDENCY_STRATEGIES),
140
+ )
141
+ .option("--layers <n>", "Layers for the `layered` strategy.", int)
142
+ .option("--fanout <n>", "Max upstream deps per project.", int)
143
+ .option(
144
+ "--edge-probability <p>",
145
+ "Edge probability for `random`.",
146
+ float,
147
+ )
148
+ .option("--log-lines <n>", "Log lines printed per task.", int)
149
+ .option("--work <n>", "CPU work iterations per task.", int)
150
+ .option("--output-files <n>", "Output files per task.", int)
151
+ .option(
152
+ "--tools <list>",
153
+ "Comma-separated tools (omni,turbo,nx,moon).",
154
+ toolList,
155
+ )
156
+ .option("--turbo-version <semver>", "Turbo version to install.")
157
+ .option("--nx-version <semver>", "Nx version to install.")
158
+ .option(
159
+ "--moon-version <semver>",
160
+ "moon (@moonrepo/cli) version to install.",
161
+ )
162
+ .option("--bun-version <semver>", "bun version for packageManager.")
163
+ .option("--no-chain", "Disable intra-project task chaining.")
164
+ .option("--no-fan-upstream", "Disable upstream (^) task dependencies.");
165
+ }
166
+
167
+ function progressHandler(): (event: BenchEvent) => void {
168
+ return (event) => {
169
+ if (event.kind === "tool-start") {
170
+ process.stderr.write(`\n▶ ${event.tool}\n`);
171
+ } else if (event.kind === "tool-error") {
172
+ process.stderr.write(` ✖ ${event.tool}: ${event.error}\n`);
173
+ } else if (event.kind === "tool-unsuccessful") {
174
+ process.stderr.write(
175
+ ` ✖ ${event.tool}: exit ${event.sample.exitCode}\n`,
176
+ );
177
+ if (event.sample.stdout) {
178
+ process.stderr.write(
179
+ ` === stdout ===\n${event.sample.stdout}\n`,
180
+ );
181
+ }
182
+ if (event.sample.stderr) {
183
+ process.stderr.write(
184
+ ` === stderr ===\n${event.sample.stderr}\n`,
185
+ );
186
+ }
187
+ } else {
188
+ const status = event.sample.ok
189
+ ? "ok"
190
+ : `exit ${event.sample.exitCode}`;
191
+ process.stderr.write(
192
+ ` ${event.scenario} ${event.run}/${event.total}: ` +
193
+ `${formatMs(event.sample.durationMs)} ` +
194
+ `(ran ${event.sample.executed} tasks, ${status})\n`,
195
+ );
196
+ }
197
+ };
198
+ }
199
+
200
+ function writeJson(file: string | undefined, result: BenchmarkResult): void {
201
+ if (!file) return;
202
+ writeFileSync(file, `${JSON.stringify(result, null, 2)}\n`);
203
+ process.stderr.write(`\nWrote results to ${file}\n`);
204
+ }
205
+
206
+ // --- generate ---------------------------------------------------------------
207
+
208
+ addGenerateOptions(
209
+ program
210
+ .command("generate")
211
+ .alias("gen")
212
+ .description("Generate a benchmark workspace at the given root dir.")
213
+ .requiredOption(
214
+ "-o, --out <dir>",
215
+ "Root dir to generate the workspace into.",
216
+ ),
217
+ ).action(async (opts) => {
218
+ const out = resolve(opts.out);
219
+ const input = buildInput(opts);
220
+ const result = await generateWorkspace(out, input);
221
+ process.stderr.write(
222
+ `Generated ${result.projects.length} projects ` +
223
+ `(${result.files.length} files) at ${out}\n` +
224
+ `Tools: ${result.config.tools.join(", ")}\n`,
225
+ );
226
+ });
227
+
228
+ // --- run ---------------------------------------------------------------------
229
+
230
+ program
231
+ .command("run")
232
+ .description("Benchmark an already-generated workspace.")
233
+ .requiredOption("-d, --dir <dir>", "Root dir of a generated workspace.")
234
+ .option("--tools <list>", "Comma-separated tools to benchmark.", toolList)
235
+ .option("--task <name>", "Task to run (defaults to the last task).")
236
+ .option("--cold-runs <n>", "Cold (uncached) runs per tool.", int, 3)
237
+ .option("--warm-runs <n>", "Warm (cached) runs per tool.", int, 5)
238
+ .option(
239
+ "--concurrency <n>",
240
+ "Max parallel tasks, applied identically to all tools (default: CPU count).",
241
+ int,
242
+ )
243
+ .option("--no-daemon", "Disable each tool's persistent daemon (turbo, nx).")
244
+ .option("--json <file>", "Write full results as JSON to this file.")
245
+ .action(async (opts) => {
246
+ const dir = resolve(opts.dir);
247
+ const result = await runBenchmark(dir, {
248
+ tools: opts.tools,
249
+ task: opts.task,
250
+ concurrency: opts.concurrency,
251
+ daemon: opts.daemon,
252
+ coldRuns: opts.coldRuns,
253
+ warmRuns: opts.warmRuns,
254
+ onEvent: progressHandler(),
255
+ });
256
+ process.stdout.write(formatReport(result));
257
+ writeJson(opts.json, result);
258
+ });
259
+
260
+ // --- bench (generate + install + run) ---------------------------------------
261
+
262
+ addGenerateOptions(
263
+ program
264
+ .command("bench")
265
+ .description("Generate, install, and benchmark in one step.")
266
+ .requiredOption(
267
+ "-o, --out <dir>",
268
+ "Root dir to generate the workspace into.",
269
+ )
270
+ .option(
271
+ "--no-install",
272
+ "Skip `bun install` in the generated workspace.",
273
+ )
274
+ .option("--task <name>", "Task to run (defaults to the last task).")
275
+ .option("--cold-runs <n>", "Cold (uncached) runs per tool.", int, 3)
276
+ .option("--warm-runs <n>", "Warm (cached) runs per tool.", int, 5)
277
+ .option(
278
+ "--concurrency <n>",
279
+ "Max parallel tasks, applied identically to all tools (default: CPU count).",
280
+ int,
281
+ )
282
+ .option(
283
+ "--no-daemon",
284
+ "Disable each tool's persistent daemon (turbo, nx).",
285
+ )
286
+ .option("--json <file>", "Write full results as JSON to this file."),
287
+ ).action(async (opts) => {
288
+ const out = resolve(opts.out);
289
+ const input = buildInput(opts);
290
+ const generated = await generateWorkspace(out, input);
291
+ process.stderr.write(
292
+ `Generated ${generated.projects.length} projects at ${out}\n`,
293
+ );
294
+
295
+ if (opts.install !== false) {
296
+ process.stderr.write("Installing dependencies (bun install)...\n");
297
+ await installWorkspace(out);
298
+ }
299
+
300
+ const result = await runBenchmark(out, {
301
+ task: opts.task,
302
+ coldRuns: opts.coldRuns,
303
+ warmRuns: opts.warmRuns,
304
+ concurrency: opts.concurrency,
305
+ daemon: opts.daemon,
306
+ onEvent: progressHandler(),
307
+ });
308
+ process.stdout.write(formatReport(result));
309
+ writeJson(opts.json, result);
310
+ });
311
+
312
+ // --- inspect (no side effects) ----------------------------------------------
313
+
314
+ addGenerateOptions(
315
+ program
316
+ .command("inspect")
317
+ .description("Resolve a config and print the derived graph summary."),
318
+ ).action((opts) => {
319
+ const config = resolveConfig(buildInput(opts));
320
+ process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
321
+ });
322
+
323
+ // --- suite (run a preset of scenarios) --------------------------------------
324
+
325
+ function suiteProgressHandler(): (event: SuiteEvent) => void {
326
+ const bench = progressHandler();
327
+ return (event) => {
328
+ if (event.kind === "scenario-start") {
329
+ process.stderr.write(
330
+ `\n=== [${event.index + 1}/${event.total}] ${event.name} ===\n`,
331
+ );
332
+ } else if (event.kind === "bench") {
333
+ bench(event.event);
334
+ }
335
+ };
336
+ }
337
+
338
+ program
339
+ .command("suite")
340
+ .description(
341
+ "Run a preset (or JSON file) of benchmark scenarios and summarize them.",
342
+ )
343
+ .option("-p, --preset <name>", "Built-in preset name.")
344
+ .option("-f, --file <path>", "Path to a JSON suite preset file.")
345
+ .option("-o, --out <dir>", "Working dir for generated workspaces.")
346
+ .option("--json <file>", "Write aggregated results as JSON to this file.")
347
+ .option(
348
+ "--md <file>",
349
+ "Write a human-readable Markdown report to this file.",
350
+ )
351
+ .option("--tools <list>", "Override tools for every scenario.", toolList)
352
+ .option("--cold-runs <n>", "Override cold runs for every scenario.", int)
353
+ .option("--warm-runs <n>", "Override warm runs for every scenario.", int)
354
+ .option(
355
+ "--concurrency <n>",
356
+ "Override concurrency for every scenario.",
357
+ int,
358
+ )
359
+ .option("--no-install", "Skip `bun install` in generated workspaces.")
360
+ .option("--keep", "Keep generated workspaces instead of removing them.")
361
+ .option("--list", "List the built-in presets and exit.")
362
+ .action(async (opts) => {
363
+ if (opts.list) {
364
+ process.stdout.write(
365
+ `Available presets:\n${listPresets()
366
+ .map((p) => ` - ${p}`)
367
+ .join("\n")}\n`,
368
+ );
369
+ return;
370
+ }
371
+
372
+ const suite = opts.file
373
+ ? parseSuite(JSON.parse(readFileSync(opts.file, "utf8")))
374
+ : getPreset(opts.preset ?? "quick");
375
+
376
+ const workdir = resolve(opts.out ?? join(tmpdir(), "task-bench-suite"));
377
+ process.stderr.write(
378
+ `Running suite "${suite.name}" (${suite.scenarios.length} scenarios) in ${workdir}\n`,
379
+ );
380
+
381
+ const overrides = {
382
+ ...(opts.tools ? { tools: opts.tools } : {}),
383
+ ...(opts.coldRuns !== undefined ? { coldRuns: opts.coldRuns } : {}),
384
+ ...(opts.warmRuns !== undefined ? { warmRuns: opts.warmRuns } : {}),
385
+ ...(opts.concurrency !== undefined
386
+ ? { concurrency: opts.concurrency }
387
+ : {}),
388
+ };
389
+
390
+ const result: SuiteResult = await runSuite(suite, {
391
+ workdir,
392
+ install: opts.install,
393
+ keep: opts.keep,
394
+ overrides,
395
+ onEvent: suiteProgressHandler(),
396
+ });
397
+
398
+ const md = formatSuiteMarkdown(result);
399
+ process.stdout.write(`\n${md}\n`);
400
+ if (opts.md) {
401
+ writeFileSync(opts.md, `${md}\n`);
402
+ process.stderr.write(`\nWrote Markdown report to ${opts.md}\n`);
403
+ }
404
+ if (opts.json) {
405
+ writeFileSync(opts.json, `${JSON.stringify(result, null, 2)}\n`);
406
+ process.stderr.write(`Wrote JSON results to ${opts.json}\n`);
407
+ }
408
+ });
409
+
410
+ program.parseAsync();
package/src/config.ts ADDED
@@ -0,0 +1,138 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * The supported inter-project dependency graph shapes. These control how much
5
+ * of a task graph a runner has to walk during discovery / scheduling, which is
6
+ * what we are trying to measure.
7
+ */
8
+ export const DEPENDENCY_STRATEGIES = [
9
+ "isolated",
10
+ "chain",
11
+ "fan-out",
12
+ "layered",
13
+ "random",
14
+ ] as const;
15
+
16
+ export const DependencyStrategySchema = z.enum(DEPENDENCY_STRATEGIES);
17
+ export type DependencyStrategy = z.infer<typeof DependencyStrategySchema>;
18
+
19
+ /** The task runners we know how to generate configuration for and benchmark. */
20
+ export const TOOLS = ["omni", "turbo", "nx", "moon"] as const;
21
+ export const ToolSchema = z.enum(TOOLS);
22
+ export type Tool = z.infer<typeof ToolSchema>;
23
+
24
+ export const DependencyConfigSchema = z
25
+ .object({
26
+ strategy: DependencyStrategySchema.default("layered").describe(
27
+ "Shape of the inter-project dependency graph.",
28
+ ),
29
+ layers: z
30
+ .number()
31
+ .int()
32
+ .positive()
33
+ .default(5)
34
+ .describe("Number of layers for the `layered` strategy."),
35
+ fanout: z
36
+ .number()
37
+ .int()
38
+ .nonnegative()
39
+ .default(3)
40
+ .describe(
41
+ "Maximum number of upstream dependencies per project (cap for `layered`/`random`).",
42
+ ),
43
+ edgeProbability: z
44
+ .number()
45
+ .min(0)
46
+ .max(1)
47
+ .default(0.35)
48
+ .describe("Edge inclusion probability for the `random` strategy."),
49
+ })
50
+ .prefault({});
51
+
52
+ export const TaskConfigSchema = z
53
+ .object({
54
+ logLines: z
55
+ .number()
56
+ .int()
57
+ .nonnegative()
58
+ .default(25)
59
+ .describe("How many log lines each task prints to stdout."),
60
+ workIterations: z
61
+ .number()
62
+ .int()
63
+ .nonnegative()
64
+ .default(150_000)
65
+ .describe(
66
+ "Iterations of cheap CPU work per task. Keep small so caching dominates.",
67
+ ),
68
+ outputFiles: z
69
+ .number()
70
+ .int()
71
+ .positive()
72
+ .default(1)
73
+ .describe("Number of output files each task writes into dist/."),
74
+ chainWithinProject: z
75
+ .boolean()
76
+ .default(true)
77
+ .describe(
78
+ "Whether task `tN` depends on `t(N-1)` within a project.",
79
+ ),
80
+ fanUpstream: z
81
+ .boolean()
82
+ .default(true)
83
+ .describe(
84
+ "Whether task `tN` depends on `tN` of upstream projects (^tN).",
85
+ ),
86
+ })
87
+ .prefault({});
88
+
89
+ export const VersionsConfigSchema = z
90
+ .object({
91
+ turbo: z.string().default("2.10.3"),
92
+ nx: z.string().default("23.0.1"),
93
+ moon: z.string().default("2.3.5"),
94
+ bun: z.string().default("1.3.14"),
95
+ })
96
+ .prefault({});
97
+
98
+ export const HarnessConfigSchema = z.object({
99
+ seed: z
100
+ .number()
101
+ .int()
102
+ .nonnegative()
103
+ .default(1)
104
+ .describe("Seed for deterministic graph generation."),
105
+ projectPrefix: z
106
+ .string()
107
+ .regex(/^[a-z][a-z0-9-]*$/)
108
+ .default("bench-p")
109
+ .describe("Prefix used for generated package names."),
110
+ projects: z
111
+ .number()
112
+ .int()
113
+ .positive()
114
+ .default(50)
115
+ .describe("Number of projects to generate."),
116
+ tasksPerProject: z
117
+ .number()
118
+ .int()
119
+ .positive()
120
+ .default(3)
121
+ .describe("Number of tasks (t0..tN-1) per project."),
122
+ dependency: DependencyConfigSchema,
123
+ task: TaskConfigSchema,
124
+ tools: z
125
+ .array(ToolSchema)
126
+ .min(1)
127
+ .default([...TOOLS])
128
+ .describe("Which runners to configure and benchmark."),
129
+ versions: VersionsConfigSchema,
130
+ });
131
+
132
+ export type HarnessConfig = z.infer<typeof HarnessConfigSchema>;
133
+ export type HarnessConfigInput = z.input<typeof HarnessConfigSchema>;
134
+
135
+ /** Parse and fill defaults for a (possibly partial) harness config. */
136
+ export function resolveConfig(input?: HarnessConfigInput): HarnessConfig {
137
+ return HarnessConfigSchema.parse(input ?? {});
138
+ }