@ultimat3/cli 22.6.2 → 22.7.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.
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,20 +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 {
8
- MEMORY_SHARE,
9
- SERIAL_TYPES,
10
- WORKER_BYTES,
11
- WORKER_CEILING,
12
- WORKER_FLOOR,
13
- WORKER_OVERSUBSCRIBE,
14
- } from './test-workers';
7
+ import { SERIAL_TYPES, WORKER_BYTES, WORKER_CEILING, WORKER_FLOOR } from './test-workers';
15
8
 
16
9
  export const testSpec: CommandSpec = {
17
10
  name: 'test',
18
11
  summary:
19
12
  'run one test type — or the whole suite — across N workers, one isolated database per worker',
20
- 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>]`,
21
14
  positionalChoices: TEST_TYPES,
22
15
  // The one command that hands a tail to another tool — `bun test` — and the reason
23
16
  // `CommandSpec.passthrough` exists: `x test unit -- --coverage --bail` parsed both flags and
@@ -27,7 +20,13 @@ export const testSpec: CommandSpec = {
27
20
  {
28
21
  name: 'workers',
29
22
  type: 'string',
30
- summary: `bun worker count (default: ${WORKER_OVERSUBSCRIBE}x CPUs rounded up, held to ${MEMORY_SHARE * 100}% of free memory at ${WORKER_BYTES / 2 ** 30} GiB a worker, 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',
31
30
  },
32
31
  {
33
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,33 +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 {
6
- MEMORY_SHARE,
7
- WORKER_BYTES,
8
- WORKER_CEILING,
9
- WORKER_FLOOR,
10
- WORKER_OVERSUBSCRIBE,
11
- } 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;
12
9
 
13
10
  export const verifySpec: CommandSpec = {
14
11
  name: 'verify',
15
12
  summary: 'the gate: typecheck, lint, boundaries, all tests, drift, contract, budgets',
16
- 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]',
17
15
  requiresApp: true,
18
- // Two flags, and only one of them narrows. `--workers` changes how wide the test steps
19
- // spread, never which steps run. `--only` runs the steps it names and says so in both renderers —
20
- // 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.
21
22
  flags: [
22
23
  {
23
24
  name: 'workers',
24
25
  type: 'string',
25
- summary: `test processes per parallel step (default: ${WORKER_OVERSUBSCRIBE}x CPUs rounded up, held to ${MEMORY_SHARE * 100}% of free memory at ${WORKER_BYTES / 2 ** 30} GiB a worker; 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})`,
26
28
  },
27
29
  {
28
30
  name: 'only',
29
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'],
30
54
  summary:
31
- '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',
32
56
  },
33
57
  ],
34
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
@@ -294,6 +294,7 @@ const CATALOG = {
294
294
  // included. Its own line and not `allSkipped` with a zero, because the repair differs — there is
295
295
  // no skipped test to read a reason off, so the files the step selected hold no test.
296
296
  'cli.verify.ranNothing': 'found no test to run in the file(s) it selected',
297
+ 'cli.verify.merged': 'merged {parts} part(s) — {summary}',
297
298
  'cli.verify.serial': 'serial',
298
299
  'cli.verify.workers': '{workers} workers',
299
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
 
@@ -31,6 +31,7 @@
31
31
  import type { ExecResult, Runner } from './exec';
32
32
  import { execOutput } from './exec';
33
33
  import { msg } from './messages';
34
+ import { SLOT_HELD_ENV } from './test-slots';
34
35
  import { BATCH_FILES_PER_WORKER } from './test-workers';
35
36
 
36
37
  /**
@@ -38,9 +39,13 @@ import { BATCH_FILES_PER_WORKER } from './test-workers';
38
39
  * in `workers x BATCH_FILES_PER_WORKER`, which is every small selection and every `--filter`.
39
40
  * Otherwise the fewest groups that fit, dealt round-robin so their sizes differ by at most one.
40
41
  */
41
- export function testBatches(files: readonly string[], workers: number): readonly string[][] {
42
+ export function testBatches(
43
+ files: readonly string[],
44
+ workers: number,
45
+ perWorker: number = BATCH_FILES_PER_WORKER,
46
+ ): readonly string[][] {
42
47
  const sorted = [...files].sort();
43
- const cap = Math.max(1, Math.trunc(workers)) * BATCH_FILES_PER_WORKER;
48
+ const cap = Math.max(1, Math.trunc(workers)) * Math.max(1, Math.trunc(perWorker));
44
49
  const count = Math.max(1, Math.ceil(sorted.length / cap));
45
50
  const batches: string[][] = Array.from({ length: count }, () => []);
46
51
  sorted.forEach((file, index) => {
@@ -53,6 +58,8 @@ export function testBatches(files: readonly string[], workers: number): readonly
53
58
  export interface BatchedRun {
54
59
  /** One per batch, in order — `countsOf` reads each one's own Bun summary. */
55
60
  readonly results: readonly ExecResult[];
61
+ /** The width each batch really ran at, after the machine lease. */
62
+ readonly widths: readonly number[];
56
63
  readonly ok: boolean;
57
64
  /** The first non-zero exit, or 0. */
58
65
  readonly code: number;
@@ -74,7 +81,15 @@ export interface BatchedRun {
74
81
  export async function runBatches(input: {
75
82
  readonly runner: Runner;
76
83
  readonly batches: readonly (readonly string[])[];
77
- readonly argsFor: (files: readonly string[]) => 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 }>;
78
93
  readonly options: Parameters<Runner>[1];
79
94
  /**
80
95
  * Launch no further batch once one is red. Set for a caller-forwarded `--bail`: Bun stops a
@@ -83,9 +98,24 @@ export async function runBatches(input: {
83
98
  readonly stopOnFailure?: boolean;
84
99
  }): Promise<BatchedRun> {
85
100
  const results: ExecResult[] = [];
101
+ const widths: number[] = [];
86
102
  for (const files of input.batches) {
87
- const result = await input.runner(input.argsFor(files), input.options);
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
+ }
88
117
  results.push(result);
118
+ widths.push(width);
89
119
  if (!result.ok && input.stopOnFailure === true) break;
90
120
  }
91
121
  const first = results.find((result) => !result.ok);
@@ -106,6 +136,7 @@ export async function runBatches(input: {
106
136
  .join('\n');
107
137
  return {
108
138
  results,
139
+ widths,
109
140
  ok: first === undefined,
110
141
  code: first?.code ?? 0,
111
142
  durationMs: results.reduce((sum, result) => sum + result.durationMs, 0),