@ultimat3/cli 22.6.1 → 22.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cmd-dev.ts CHANGED
@@ -13,8 +13,10 @@ import type { OverlayNotice, RequestContext } from '@ultimat3/http';
13
13
  import { asCtx } from '@ultimat3/http';
14
14
  import type { Manifest } from '@ultimat3/manifest';
15
15
  import { MANIFEST_FILENAME } from '@ultimat3/manifest';
16
+ import { createIsrController } from '@ultimat3/render/server';
16
17
  import { loadSignInPath } from './app-auth';
17
18
  import { appManifest } from './app-manifest';
19
+ import { enableReloadTracking } from './app-reload-graph';
18
20
  import { requireAppRoot } from './app-root';
19
21
  import { loadAppRuntime } from './app-runtime';
20
22
  import { devSpec } from './cmd-dev-spec';
@@ -128,6 +130,9 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
128
130
  // EVERY boot: a scratch server (`x shot`, `ui.shot`) boots here too, and without this a
129
131
  // fail-closed dev actor installs nothing — the picture is of a 401. Idempotent.
130
132
  declareDevEnvironment(options.env);
133
+ // Before the first scan: the watcher's rescans evict a saved module AND everything importing it,
134
+ // and that needs the import graph recorded as each module is first loaded (`app-reload-graph.ts`).
135
+ enableReloadTracking(true);
131
136
  // The declaration lands on `process.env`; the env this boot passes on must carry it too, or every
132
137
  // service resolved from `options.env` still reads no `ULTIMATE_ENV`.
133
138
  const env = needsDevEnvironmentDeclaration(options.env)
@@ -224,7 +229,11 @@ async function bootDev(
224
229
  };
225
230
  const panels = devPanels(dashboard).map((panel) => panel.key);
226
231
 
232
+ // `x dev`'s own, so a reload can empty it: an `isr` page's first render was otherwise served for
233
+ // its whole ttl after every save, whatever the modules behind it now said.
234
+ const isr = createIsrController({ buildId });
227
235
  const { routes, theme, errorStyles, mcpPath } = await devRouteTable({
236
+ isr,
228
237
  root: options.root,
229
238
  env: options.env,
230
239
  buildId,
@@ -283,6 +292,7 @@ async function bootDev(
283
292
  state.manifest = manifest;
284
293
  state.appFindings = findings;
285
294
  state.islands = islands;
295
+ for (const path of isr.store().paths()) isr.store().delete(path);
286
296
  state.reloads += 1;
287
297
  state.reloadFinding = undefined;
288
298
  options.onReload?.(file, Math.round(performance.now() - started));
@@ -4,13 +4,13 @@
4
4
  import { TEST_TYPES } from '@ultimat3/testing/test-types';
5
5
  import { DEFAULT_BASE } from './affected';
6
6
  import type { CommandSpec } from './parse';
7
- import { SERIAL_TYPES, WORKER_CEILING, WORKER_FLOOR, WORKER_OVERSUBSCRIBE } from './test-workers';
7
+ import { SERIAL_TYPES, WORKER_BYTES, WORKER_CEILING, WORKER_FLOOR } from './test-workers';
8
8
 
9
9
  export const testSpec: CommandSpec = {
10
10
  name: 'test',
11
11
  summary:
12
12
  'run one test type — or the whole suite — across N workers, one isolated database per worker',
13
- usage: `x test [${TEST_TYPES.join('|')}] [--filter path[,path…]] [--allow-empty] [--sample N] [--affected [--base ref] [--dirty]] [--workers N] [--worker I] [--json] [-- <bun test flags>]`,
13
+ usage: `x test [${TEST_TYPES.join('|')}] [--filter path[,path…]] [--allow-empty] [--sample N] [--affected [--base ref] [--dirty]] [--workers N] [--worker I] [--isolate] [--json] [-- <bun test flags>]`,
14
14
  positionalChoices: TEST_TYPES,
15
15
  // The one command that hands a tail to another tool — `bun test` — and the reason
16
16
  // `CommandSpec.passthrough` exists: `x test unit -- --coverage --bail` parsed both flags and
@@ -20,7 +20,13 @@ export const testSpec: CommandSpec = {
20
20
  {
21
21
  name: 'workers',
22
22
  type: 'string',
23
- summary: `bun worker count (default: ${WORKER_OVERSUBSCRIBE}x CPUs rounded up, held to free memory, min ${WORKER_FLOOR}; max ${WORKER_CEILING}); clamped to the file count, and to 1 for ${SERIAL_TYPES.join(' and ')}`,
23
+ summary: `bun worker count (default: one per ${WORKER_BYTES / 2 ** 30} GiB of a min(4 GiB, max(2.75 GiB, 25% of RAM)) budget, at most one per core; ULTIMATE_TEST_MEMORY_BUDGET and ULTIMATE_TEST_MAX_WORKERS override, min ${WORKER_FLOOR}; max ${WORKER_CEILING}); clamped to the file count, and to 1 for ${SERIAL_TYPES.join(' and ')}`,
24
+ },
25
+ {
26
+ name: 'isolate',
27
+ type: 'boolean',
28
+ summary:
29
+ 'a fresh global per test file (bun test --isolate) — off by default since 22.7; x.verify.json "isolate": true makes it the repo default',
24
30
  },
25
31
  {
26
32
  name: 'worker',
package/src/cmd-test.ts CHANGED
@@ -26,7 +26,16 @@ import {
26
26
  sampleFiles,
27
27
  } from './test-select';
28
28
  import { runShards } from './test-shards';
29
- import { defaultWorkers, SERIAL_TYPES, WORKER_CEILING } from './test-workers';
29
+ import { machineLease } from './test-slots';
30
+ import type { WorkerPlan } from './test-workers';
31
+ import {
32
+ availableCpus,
33
+ SERIAL_TYPES,
34
+ totalMemory,
35
+ WORKER_CEILING,
36
+ workerPlan,
37
+ } from './test-workers';
38
+ import { readVerifyFloor } from './verify-floor';
30
39
  import type { TestType } from './verify-tests';
31
40
  import { TEST_TYPES } from './verify-tests';
32
41
 
@@ -159,7 +168,12 @@ export const testCommand: CliCommand = {
159
168
  });
160
169
  }
161
170
  const files = sample === undefined ? selected : sampleFiles(selected, sample);
162
- const requested = readIndex(ctx.args, 'workers', 1) ?? defaultWorkers();
171
+ const explicit = readIndex(ctx.args, 'workers', 1);
172
+ const plan =
173
+ explicit === undefined ? workerPlan(availableCpus(), totalMemory(), ctx.env) : undefined;
174
+ const requested = explicit ?? (plan as WorkerPlan).workers;
175
+ const floor = await readVerifyFloor(ctx.cwd);
176
+ const isolate = flagBool(ctx.args, 'isolate') || floor?.isolate === true;
163
177
  // The width a `--worker` index is judged against, and nothing else: which files really run one
164
178
  // at a time is `test-passes.ts`, off the FILES rather than off the positional. This line read
165
179
  // the positional alone until 2026-09 and the comment here claimed it made a serial type serial
@@ -183,6 +197,10 @@ export const testCommand: CliCommand = {
183
197
  env: ctx.env,
184
198
  files,
185
199
  workers,
200
+ ...(isolate ? { isolate } : {}),
201
+ // A default-width run leases its workers from the machine pool, so a second `x test` or
202
+ // gate on the same box runs narrower instead of doubling its memory.
203
+ ...(plan === undefined ? {} : withLease(machineLease(plan.workers, ctx.env))),
186
204
  ...(only === undefined ? {} : { only }),
187
205
  ...(filter === undefined ? {} : { filter }),
188
206
  ...(type === undefined ? {} : { type }),
@@ -201,3 +219,8 @@ export const testCommand: CliCommand = {
201
219
  return scope === undefined ? result : withScope(result, scope);
202
220
  },
203
221
  };
222
+
223
+ const withLease = (
224
+ lease: ReturnType<typeof machineLease>,
225
+ ): { lease?: NonNullable<ReturnType<typeof machineLease>> } =>
226
+ lease === undefined ? {} : { lease };
@@ -2,27 +2,57 @@
2
2
  // read it without loading `cmd-verify.ts`, which `registry.ts` imports only when the command runs.
3
3
 
4
4
  import type { CommandSpec } from './parse';
5
- import { WORKER_CEILING, WORKER_FLOOR, WORKER_OVERSUBSCRIBE } from './test-workers';
5
+ import { WORKER_BYTES, WORKER_CEILING, WORKER_FLOOR } from './test-workers';
6
+
7
+ /** `run` is the gate (and what a bare `x verify` means); `merge` folds CI parts into its verdict. */
8
+ export const VERIFY_SUBCOMMANDS = ['run', 'merge'] as const;
6
9
 
7
10
  export const verifySpec: CommandSpec = {
8
11
  name: 'verify',
9
12
  summary: 'the gate: typecheck, lint, boundaries, all tests, drift, contract, budgets',
10
- usage: 'x verify [--only <step>[,<step>…]] [--workers N] [--json]',
13
+ usage:
14
+ 'x verify [--only <step>[,<step>…] [--shard i/n [--timings file]]] [--workers N] [--isolate] [--json] · x verify merge <part.json…> [--json]',
11
15
  requiresApp: true,
12
- // Two flags, and only one of them narrows. `--workers` changes how wide the test steps
13
- // spread, never which steps run. `--only` runs the steps it names and says so in both renderers —
14
- // never silently, which is the whole of what makes it safe to have.
16
+ subcommands: VERIFY_SUBCOMMANDS,
17
+ defaultSubcommand: 'run',
18
+ // `--workers` and `--isolate` change how the test steps run, never which steps run. `--only`
19
+ // runs the steps it names and says so in both renderers — never silently, which is the whole
20
+ // of what makes it safe to have. `--shard` narrows further, and only `merge` turns shards back
21
+ // into a gate verdict.
15
22
  flags: [
16
23
  {
17
24
  name: 'workers',
18
25
  type: 'string',
19
- summary: `test processes per parallel step (default: ${WORKER_OVERSUBSCRIBE}x CPUs rounded up, held to free memory; min ${WORKER_FLOOR}, max ${WORKER_CEILING})`,
26
+ subcommands: ['run'],
27
+ summary: `test processes per parallel step (default: one per ${WORKER_BYTES / 2 ** 30} GiB of a min(4 GiB, max(2.75 GiB, 25% of RAM)) budget, at most one per core; ULTIMATE_TEST_MEMORY_BUDGET and ULTIMATE_TEST_MAX_WORKERS override; min ${WORKER_FLOOR}, max ${WORKER_CEILING})`,
20
28
  },
21
29
  {
22
30
  name: 'only',
23
31
  type: 'string',
32
+ subcommands: ['run'],
33
+ summary:
34
+ 'run the named step(s), comma-separated, in one process — an iteration loop or one CI job, NOT A GATE RUN; the gate is this command with no flag, or `x verify merge` over every job',
35
+ },
36
+ {
37
+ name: 'shard',
38
+ type: 'string',
39
+ subcommands: ['run'],
40
+ summary:
41
+ 'with --only unit|contract|job: run shard i of an n-way split of each step’s sorted file list (round-robin), for one CI job; `x verify merge` checks all n',
42
+ },
43
+ {
44
+ name: 'timings',
45
+ type: 'string',
46
+ subcommands: ['run'],
47
+ summary:
48
+ 'with --shard: a bun --timings JSON ({ "path": ms }) — split greedy longest-first instead of round-robin',
49
+ },
50
+ {
51
+ name: 'isolate',
52
+ type: 'boolean',
53
+ subcommands: ['run'],
24
54
  summary:
25
- 'run the named step(s), comma-separated, in one process — an iteration loop, NOT A GATE RUN; the gate is this command with no flag',
55
+ 'a fresh global per test file (bun test --isolate) — off by default since 22.7; x.verify.json "isolate": true makes it the repo default',
26
56
  },
27
57
  ],
28
58
  };
package/src/cmd-verify.ts CHANGED
@@ -7,18 +7,26 @@
7
7
  // reader of either can take it for one. `--skip` stays refused — it would let a caller drop the
8
8
  // step that was going to fail and still read the output as a whole-tree verdict.
9
9
 
10
+ // why: Bun ships no path-joining primitive: `join`/`isAbsolute` resolve a part path against the cwd.
11
+ import { isAbsolute, join } from 'node:path';
10
12
  import { nearestName, renderFixShellArg } from '@ultimat3/core';
11
13
  import { requireAppRoot } from './app-root';
12
14
  import { verifySpec } from './cmd-verify-spec';
13
15
  import type { CliCommand, CommandContext } from './command';
14
- import { BadFlagError } from './errors';
16
+ import { BadFlagError, MissingPositionalError } from './errors';
15
17
  import { readIntFlag } from './flag-number';
16
- import type { CommandResult } from './output';
18
+ import type { CommandResult, JsonValue } from './output';
17
19
  import type { ParsedArgs } from './parse';
18
- import { flagString } from './parse';
20
+ import { flagBool, flagString } from './parse';
19
21
  import { WORKER_CEILING, WORKER_FLOOR } from './test-workers';
20
22
  import { VERIFY_STEPS } from './verify-checks';
23
+ import { VerifyMergeInputError, VerifyShardInvalidError } from './verify-errors';
24
+ import { readVerifyFloor } from './verify-floor';
25
+ import type { VerifyPart } from './verify-merge';
26
+ import { mergeParts, parsePart } from './verify-merge';
21
27
  import { runVerify } from './verify-run';
28
+ import type { ShardSpec, Timings } from './verify-shard';
29
+ import { assertShardable, parseShard, readTimings } from './verify-shard';
22
30
  import type { VerifyStepName } from './verify-step';
23
31
  import { VERIFY_STEP_NAMES } from './verify-step';
24
32
 
@@ -32,20 +40,88 @@ export const verifyCommand: CliCommand = {
32
40
  spec: verifySpec,
33
41
  async run(ctx: CommandContext): Promise<CommandResult> {
34
42
  const root = requireAppRoot('verify', ctx.cwd).dir;
35
- // Both readers before the run: an unrunnable flag must be refused in milliseconds, not after
43
+ if (ctx.args.subcommand === 'merge') return mergeCommand(root, ctx.args.positionals, ctx.cwd);
44
+ // Every reader before the run: an unrunnable flag must be refused in milliseconds, not after
36
45
  // `tsc -b` has spent fourteen seconds on a run the caller cannot use.
37
46
  const workers = readWorkers(ctx.args);
38
47
  const only = readOnlySteps(ctx.args);
39
- return runVerify(VERIFY_STEPS, {
48
+ const shard = await readShard(ctx.args, only);
49
+ const isolate = flagBool(ctx.args, 'isolate') ? true : undefined;
50
+ const result = await runVerify(VERIFY_STEPS, {
40
51
  root,
41
52
  runner: ctx.runner,
42
53
  env: ctx.env,
43
54
  ...(workers === undefined ? {} : { workers }),
44
55
  ...(only === undefined ? {} : { only }),
56
+ ...(shard === undefined ? {} : { shard }),
57
+ ...(isolate === undefined ? {} : { isolate }),
45
58
  });
59
+ return shard === undefined ? result : withShardData(result, shard);
46
60
  },
47
61
  };
48
62
 
63
+ /**
64
+ * `--shard i/n` (and `--timings`), validated before anything runs: only beside `--only`, and only
65
+ * over the parallel suites. `--timings` without `--shard` is refused rather than ignored.
66
+ */
67
+ export async function readShard(
68
+ args: ParsedArgs,
69
+ only: readonly VerifyStepName[] | undefined,
70
+ ): Promise<(ShardSpec & { readonly timings?: Timings }) | undefined> {
71
+ const raw = flagString(args, 'shard');
72
+ const timingsPath = flagString(args, 'timings');
73
+ if (raw === undefined) {
74
+ if (timingsPath !== undefined) {
75
+ throw new VerifyShardInvalidError({
76
+ reason: '--timings balances a --shard split and does nothing without one',
77
+ });
78
+ }
79
+ return undefined;
80
+ }
81
+ const spec = parseShard(raw);
82
+ assertShardable(only);
83
+ if (timingsPath === undefined) return spec;
84
+ return { ...spec, timings: await readTimings(timingsPath) };
85
+ }
86
+
87
+ /** `data.shard`: which slice this part is, per step — what `x verify merge` reads back. */
88
+ const withShardData = (result: CommandResult, shard: ShardSpec): CommandResult => {
89
+ const data = (result.data ?? {}) as Record<string, JsonValue>;
90
+ const steps: Record<string, JsonValue> = {};
91
+ for (const step of result.steps ?? []) {
92
+ if (step.shard === undefined) continue;
93
+ steps[step.name] = { corpusHash: step.shard.corpusHash, files: [...step.shard.files] };
94
+ }
95
+ return {
96
+ ...result,
97
+ data: { ...data, shard: { index: shard.index, total: shard.total, steps } },
98
+ };
99
+ };
100
+
101
+ async function mergeCommand(
102
+ root: string,
103
+ files: readonly string[],
104
+ cwd: string,
105
+ ): Promise<CommandResult> {
106
+ if (files.length === 0) {
107
+ throw new MissingPositionalError({
108
+ command: 'verify merge',
109
+ positional: 'part.json…',
110
+ example: 'x verify merge parts/*.json --json',
111
+ });
112
+ }
113
+ const parts: VerifyPart[] = [];
114
+ for (const file of files) {
115
+ const path = isAbsolute(file) ? file : join(cwd, file);
116
+ const handle = Bun.file(path);
117
+ if (!(await handle.exists())) {
118
+ throw new VerifyMergeInputError({ file, reason: 'does not exist' });
119
+ }
120
+ parts.push(parsePart(file, await handle.text()));
121
+ }
122
+ return mergeParts(parts, await readVerifyFloor(root));
123
+ }
124
+
49
125
  /**
50
126
  * The steps `--only` names, or nothing: one step (`--only lint`) or a comma-separated list
51
127
  * (`--only typecheck,lint,boundaries`) run in ONE process, in the gate's declared order whatever
@@ -5,6 +5,7 @@
5
5
  import type { RealtimeConfig } from '@ultimat3/core';
6
6
  import type { RateLimitStore, Route } from '@ultimat3/http';
7
7
  import { describeRoutes } from '@ultimat3/render';
8
+ import type { IsrController } from '@ultimat3/render/server';
8
9
  import type { Storage } from '@ultimat3/storage';
9
10
  import { apiMountRoutes, apiRoutes, pagePostRoutes } from './api-routes';
10
11
  import { mountAppMcp } from './app-mcp';
@@ -43,6 +44,11 @@ export interface DevRouteTableInput {
43
44
  readonly rateLimitStore?: RateLimitStore | undefined;
44
45
  /** `apps/<app>/runtime.ts`'s plain `routes`, mounted as the container mounts them. */
45
46
  readonly appRoutes?: readonly Route[] | undefined;
47
+ /**
48
+ * The ISR controller the page routes render through — `x dev`'s own, because a reload has to
49
+ * empty it: a stored render outlives the module that produced it.
50
+ */
51
+ readonly isr?: IsrController | undefined;
46
52
  }
47
53
 
48
54
  export interface DevRouteTable {
@@ -126,6 +132,7 @@ export async function devRouteTable(input: DevRouteTableInput): Promise<DevRoute
126
132
  ...pagePostRoutes(),
127
133
  ...appRoutes({
128
134
  buildId: input.buildId,
135
+ ...(input.isr === undefined ? {} : { isr: input.isr }),
129
136
  resolveIsland: (file) => input.islands().resolverFor(file),
130
137
  ...(sync.head === undefined ? {} : { sync: sync.head }),
131
138
  persisted: sync.persisted,
@@ -198,6 +198,10 @@ export const CLI_OWNED_ERROR_CODES = [
198
198
  'X_FEATURE_UNKNOWN',
199
199
  'X_ROUTE_ASYNC_PAGE',
200
200
  'X_BUDGET_PARAMS_UNDECLARED',
201
+ 'X_TEST_BUDGET_INVALID',
202
+ 'X_VERIFY_SHARD_INVALID',
203
+ 'X_VERIFY_MERGE_INCOMPLETE',
204
+ 'X_VERIFY_MERGE_INPUT',
201
205
  ] as const;
202
206
 
203
207
  /**
@@ -345,6 +349,10 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
345
349
  X_ROUTE_ASYNC_PAGE: 'a route exports an async Page',
346
350
  X_BUDGET_PARAMS_UNDECLARED:
347
351
  'a dynamic route with a budget declares no prerender() paths to weigh it by',
352
+ X_TEST_BUDGET_INVALID: 'a test memory budget or worker cap env var does not parse',
353
+ X_VERIFY_SHARD_INVALID: 'x verify --shard was given a spec or a step list it cannot split',
354
+ X_VERIFY_MERGE_INCOMPLETE: 'x verify merge was handed parts that do not add up to the whole gate',
355
+ X_VERIFY_MERGE_INPUT: 'a part handed to x verify merge is not an x verify --json document',
348
356
  };
349
357
 
350
358
  // One unconditional call, so a second package claiming one of the CLI's codes throws
package/src/mcp-errors.ts CHANGED
@@ -38,6 +38,11 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
38
38
  X_ENV_MISSING: 'x env check --json',
39
39
  X_ENV_EXAMPLE_DRIFT: 'x env example --json',
40
40
  X_TEST_NO_FILES: 'x test --json # from the repo root, or pass --cwd to it',
41
+ X_TEST_BUDGET_INVALID: 'ULTIMATE_TEST_MEMORY_BUDGET=3g x verify --json',
42
+ X_VERIFY_SHARD_INVALID: 'x verify --only unit --shard 1/4 --json',
43
+ X_VERIFY_MERGE_INCOMPLETE:
44
+ 'x verify merge parts/*.json --json # after every CI job uploaded its part',
45
+ X_VERIFY_MERGE_INPUT: 'x verify --only unit --json > part.json # one document per part',
41
46
  X_TEST_SHARD_FAILED: 'x test --workers 1 --json',
42
47
  X_SCAFFOLD_PATH_ESCAPE: 'x g route posts --json # a path with no ".." segment',
43
48
  X_GENERATE_JSON_INVALID:
package/src/messages.ts CHANGED
@@ -265,6 +265,8 @@ const CATALOG = {
265
265
  'cli.test.affected.none': 'nothing is affected by {base}...HEAD — 0 test file(s) ran',
266
266
  'cli.test.empty': 'no test file matches {selection} — 0 test file(s) ran (--allow-empty)',
267
267
  'cli.test.pass': '{files} test file(s) on {workers} worker(s) passed in {ms}ms',
268
+ 'cli.test.batch': '── batch {batch} of {batches} · {files} file(s) ──',
269
+ 'cli.test.batches': ' · {batches} batches',
268
270
  'cli.test.sampled': 'sampled {kept} of {total} {type} file(s)',
269
271
  'cli.test.type.fail': '{type} — {failed} of {workers} shard(s) failed',
270
272
  'cli.test.type.pass': '{type} — {files} test file(s) on {workers} worker(s) passed in {ms}ms',
@@ -292,6 +294,7 @@ const CATALOG = {
292
294
  // included. Its own line and not `allSkipped` with a zero, because the repair differs — there is
293
295
  // no skipped test to read a reason off, so the files the step selected hold no test.
294
296
  'cli.verify.ranNothing': 'found no test to run in the file(s) it selected',
297
+ 'cli.verify.merged': 'merged {parts} part(s) — {summary}',
295
298
  'cli.verify.serial': 'serial',
296
299
  'cli.verify.workers': '{workers} workers',
297
300
  'cli.env.checked': '{count} declared variable(s), all present and valid',
package/src/output.ts CHANGED
@@ -6,6 +6,16 @@ import { ERROR_DOCS_URL, renderThrowable, singleLine, stringField } from '@ultim
6
6
  import { msg } from './messages';
7
7
  import type { TestCounts } from './test-counts';
8
8
 
9
+ /** One shard's share of one step's corpus — what `x verify merge` checks the parts against. */
10
+ export interface ShardFacts {
11
+ readonly index: number;
12
+ readonly total: number;
13
+ /** sha256 of the step's WHOLE sorted file list: every shard of one split carries the same one. */
14
+ readonly corpusHash: string;
15
+ /** The files this shard ran. */
16
+ readonly files: readonly string[];
17
+ }
18
+
9
19
  export interface Finding {
10
20
  readonly code: string;
11
21
  readonly cause: string;
@@ -31,6 +41,10 @@ export interface StepResult {
31
41
  readonly output?: string;
32
42
  /** Worker processes the step used; `1` means it ran serially. Absent for a non-test step. */
33
43
  readonly workers?: number;
44
+ /** Why the width is what it is — `4 workers (budget 4.0 GB)`. */
45
+ readonly widthReason?: string;
46
+ /** Under `x verify --shard`: this step's slice of its corpus. */
47
+ readonly shard?: ShardFacts;
34
48
  /**
35
49
  * What the step's suite executed. Absent for a step that spawned no test process, which is NOT
36
50
  * the same state as `{ ran: 0 }` — a step with no suite and a suite whose every test skipped
@@ -175,6 +189,7 @@ const mark = (step: StepResult): string => {
175
189
  */
176
190
  const width = (step: StepResult): string => {
177
191
  if (step.workers === undefined || step.skipped === true) return '';
192
+ if (step.widthReason !== undefined) return ` ${singleLine(step.widthReason)}`;
178
193
  return ` ${step.workers === 1 ? msg('cli.verify.serial') : msg('cli.verify.workers', { workers: step.workers })}`;
179
194
  };
180
195
 
@@ -227,6 +242,17 @@ export function renderJson(result: CommandResult): string {
227
242
  skipped: step.skipped === true,
228
243
  findings: step.findings,
229
244
  ...(step.workers === undefined ? {} : { workers: step.workers }),
245
+ ...(step.widthReason === undefined ? {} : { widthReason: step.widthReason }),
246
+ ...(step.shard === undefined
247
+ ? {}
248
+ : {
249
+ shard: {
250
+ index: step.shard.index,
251
+ total: step.shard.total,
252
+ corpusHash: step.shard.corpusHash,
253
+ files: [...step.shard.files],
254
+ },
255
+ }),
230
256
  // The counts the human line's `why()` renders, as numbers: a `--json` reader deciding whether
231
257
  // a skipped lane is missing a suite or missing a prerequisite needs the same fact CI's log has.
232
258
  ...(step.tests === undefined ? {} : { tests: step.tests }),
@@ -174,7 +174,9 @@ build_flags=""
174
174
  for arg in "$@"; do
175
175
  case "$arg" in --json|-j) build_flags="--json" ;; esac
176
176
  done
177
- bunx x build --target static $build_flags
177
+ # \`--no-preflight\`: the build's own preflight is x verify's first six steps (typecheck, lint,
178
+ # boundaries, filesize, package-shape, errors), and the gate below runs them anyway — once is enough.
179
+ bunx x build --target static --no-preflight $build_flags
178
180
  exec bunx x verify "$@"
179
181
  `;
180
182
 
@@ -0,0 +1,145 @@
1
+ // Spending one pass as a few `bun test --parallel=N` processes in sequence, not one. The width of
2
+ // a pass is `test-workers.ts`'s to decide; how many files one set of N workers is allowed to chew
3
+ // through before it is thrown away is this file's.
4
+ //
5
+ // WHY THERE IS MORE THAN ONE PROCESS, `As of 2026-09-27`. A `--parallel` worker is not freed file
6
+ // by file: it keeps its heap for the life of the `bun test` process and it GROWS with every file it
7
+ // is handed (JIT, module-resolution caches, PGlite heaps the collector has not yet returned). On
8
+ // notificado.co's unit corpus (768 files, PGlite per file, 12-core box, Bun 1.4.0), whole-tree RSS
9
+ // at 12 workers:
10
+ //
11
+ // | form | peak tree RSS | wall |
12
+ // |---------------------------------------------|---------------|--------|
13
+ // | 1x `--parallel=12`, 768 files (~64/worker) | 12.4-13.3 GB | 134-158s |
14
+ // | 3x `--parallel=12`, 256 files (~21/worker) | 10.8-11.4 GB | 133-147s |
15
+ // | 6x `--parallel=12`, 128 files (~11/worker) | 9.7 GB | 166s |
16
+ //
17
+ // (Wall is noisy — the box was shared, at load 30-40 — but batching at the same width never
18
+ // measured slower than not batching outside one outlier run.)
19
+ //
20
+ // One process's footprint scales with the files each worker sees, so an unbatched run's peak is a
21
+ // function of the CORPUS: every test file an app adds raises it, and nothing ever gives it back
22
+ // until the run ends. A batch boundary gives all of it back. At `BATCH_FILES_PER_WORKER` files per
23
+ // worker the peak is a function of the WIDTH alone — and the wall clock does not move, because a
24
+ // worker restart costs a second against a batch of ~40s. Halving the batch again bought 1.5 GB for
25
+ // 25% more wall, which is why the constant is not smaller.
26
+ //
27
+ // THE DEAL IS ROUND-ROBIN over the sorted list, so every batch gets a slice of every directory
28
+ // (a slow directory is not one batch's tail) and the split is a pure function of the file list and
29
+ // the width: `x test unit --workers N` reruns exactly the batches the failing run ran.
30
+
31
+ import type { ExecResult, Runner } from './exec';
32
+ import { execOutput } from './exec';
33
+ import { msg } from './messages';
34
+ import { SLOT_HELD_ENV } from './test-slots';
35
+ import { BATCH_FILES_PER_WORKER } from './test-workers';
36
+
37
+ /**
38
+ * The file groups one pass runs as, in order. One group — the list itself — whenever the list fits
39
+ * in `workers x BATCH_FILES_PER_WORKER`, which is every small selection and every `--filter`.
40
+ * Otherwise the fewest groups that fit, dealt round-robin so their sizes differ by at most one.
41
+ */
42
+ export function testBatches(
43
+ files: readonly string[],
44
+ workers: number,
45
+ perWorker: number = BATCH_FILES_PER_WORKER,
46
+ ): readonly string[][] {
47
+ const sorted = [...files].sort();
48
+ const cap = Math.max(1, Math.trunc(workers)) * Math.max(1, Math.trunc(perWorker));
49
+ const count = Math.max(1, Math.ceil(sorted.length / cap));
50
+ const batches: string[][] = Array.from({ length: count }, () => []);
51
+ sorted.forEach((file, index) => {
52
+ batches[index % count]?.push(file);
53
+ });
54
+ return batches;
55
+ }
56
+
57
+ /** Every batch's own result, and what they add up to. */
58
+ export interface BatchedRun {
59
+ /** One per batch, in order — `countsOf` reads each one's own Bun summary. */
60
+ readonly results: readonly ExecResult[];
61
+ /** The width each batch really ran at, after the machine lease. */
62
+ readonly widths: readonly number[];
63
+ readonly ok: boolean;
64
+ /** The first non-zero exit, or 0. */
65
+ readonly code: number;
66
+ /** Summed: the batches ran one after another. */
67
+ readonly durationMs: number;
68
+ /**
69
+ * Every batch's output, each under a line naming it when there was more than one. One batch is
70
+ * `execOutput` of it, byte for byte, so a small run reads exactly as it always has.
71
+ */
72
+ readonly output: string;
73
+ }
74
+
75
+ /**
76
+ * Run `batches` one after another — never `Promise.all`, which would put every batch's workers on
77
+ * the machine at once and undo the bound — and keep going after a red one, because the caller
78
+ * asked for the whole selection and a report that stops at the first failure hides the rest. The
79
+ * one exception is a caller that asked to stop (`stopOnFailure`, i.e. `-- --bail`).
80
+ */
81
+ export async function runBatches(input: {
82
+ readonly runner: Runner;
83
+ readonly batches: readonly (readonly string[])[];
84
+ /** `width` is the batch's real worker count — the ask, narrowed by the machine lease. */
85
+ readonly argsFor: (files: readonly string[], width: number) => readonly string[];
86
+ /** The width each batch asks for; required with `lease`. */
87
+ readonly workers?: number;
88
+ /**
89
+ * Lease the batch's workers from the machine pool (`test-slots.ts`) before it starts, and give
90
+ * them back when it exits — so two runs on one box share its budget batch by batch.
91
+ */
92
+ readonly lease?: (want: number) => Promise<{ readonly count: number; release(): void }>;
93
+ readonly options: Parameters<Runner>[1];
94
+ /**
95
+ * Launch no further batch once one is red. Set for a caller-forwarded `--bail`: Bun stops a
96
+ * process at its threshold, and without this the next batch would start over from zero.
97
+ */
98
+ readonly stopOnFailure?: boolean;
99
+ }): Promise<BatchedRun> {
100
+ const results: ExecResult[] = [];
101
+ const widths: number[] = [];
102
+ for (const files of input.batches) {
103
+ const want = Math.max(1, Math.min(input.workers ?? files.length, files.length));
104
+ const lease = input.lease === undefined ? undefined : await input.lease(want);
105
+ const width = Math.min(want, lease?.count ?? want);
106
+ let result: ExecResult;
107
+ try {
108
+ result = await input.runner(
109
+ input.argsFor(files, width),
110
+ lease === undefined
111
+ ? input.options
112
+ : { ...input.options, env: { ...input.options.env, [SLOT_HELD_ENV]: '1' } },
113
+ );
114
+ } finally {
115
+ lease?.release();
116
+ }
117
+ results.push(result);
118
+ widths.push(width);
119
+ if (!result.ok && input.stopOnFailure === true) break;
120
+ }
121
+ const first = results.find((result) => !result.ok);
122
+ const output =
123
+ results.length === 1
124
+ ? execOutput(results[0] as ExecResult)
125
+ : results
126
+ .map((result, index) =>
127
+ [
128
+ msg('cli.test.batch', {
129
+ batch: index + 1,
130
+ batches: results.length,
131
+ files: input.batches[index]?.length ?? 0,
132
+ }),
133
+ execOutput(result),
134
+ ].join('\n'),
135
+ )
136
+ .join('\n');
137
+ return {
138
+ results,
139
+ widths,
140
+ ok: first === undefined,
141
+ code: first?.code ?? 0,
142
+ durationMs: results.reduce((sum, result) => sum + result.durationMs, 0),
143
+ output,
144
+ };
145
+ }