@ultimat3/cli 1.1.0 → 2.0.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/CLAUDE.md +724 -0
- package/README.md +41 -9
- package/package.json +25 -23
- package/src/api-routes.ts +16 -0
- package/src/app-auth.ts +32 -0
- package/src/app-entities.ts +18 -0
- package/src/app-env.ts +103 -0
- package/src/app-load.ts +20 -3
- package/src/bin.ts +4 -3
- package/src/budgets.ts +114 -9
- package/src/cmd-build.ts +69 -21
- package/src/cmd-db-branch.ts +215 -0
- package/src/cmd-db.ts +332 -155
- package/src/cmd-deploy.ts +59 -6
- package/src/cmd-dev.ts +87 -17
- package/src/cmd-docs.ts +167 -0
- package/src/cmd-doctor.ts +64 -9
- package/src/cmd-env.ts +95 -0
- package/src/cmd-errors.ts +33 -13
- package/src/cmd-fix.ts +5 -1
- package/src/cmd-generate.ts +146 -111
- package/src/cmd-help.ts +16 -5
- package/src/cmd-i18n.ts +2 -0
- package/src/cmd-jobs.ts +47 -33
- package/src/cmd-mcp.ts +11 -2
- package/src/cmd-new.ts +13 -7
- package/src/cmd-planned.ts +55 -10
- package/src/cmd-policy.ts +1 -0
- package/src/cmd-registries.ts +3 -0
- package/src/cmd-secrets.ts +368 -0
- package/src/cmd-tasks.ts +1 -0
- package/src/cmd-test.ts +17 -23
- package/src/cmd-verify.ts +177 -23
- package/src/db-backfill.ts +401 -0
- package/src/db-branch.ts +251 -0
- package/src/db-destructive.ts +29 -0
- package/src/db-finding.ts +28 -0
- package/src/db-generate.ts +112 -0
- package/src/db-snapshot.ts +24 -0
- package/src/dev-assets.ts +86 -20
- package/src/dev-cache.ts +122 -0
- package/src/dev-dashboard.ts +19 -4
- package/src/dev-hooks.ts +27 -2
- package/src/dev-n-plus-one.ts +191 -0
- package/src/dev-queue.ts +105 -19
- package/src/dev-render.ts +158 -26
- package/src/dev-roles-fixture.ts +67 -0
- package/src/dev-roles.ts +186 -78
- package/src/dev-runtime.ts +117 -40
- package/src/dev-services.ts +15 -0
- package/src/dev-storage.ts +245 -0
- package/src/dev-sync.ts +107 -0
- package/src/dev-traces.ts +11 -3
- package/src/dispatch.ts +4 -2
- package/src/document-styles.ts +54 -0
- package/src/drift.ts +37 -9
- package/src/error-catalog.ts +7 -18
- package/src/error-codes.ts +186 -0
- package/src/error-contract.ts +29 -7
- package/src/error-fixes.ts +114 -0
- package/src/errors.ts +205 -140
- package/src/fix-command.ts +268 -0
- package/src/flag-number.ts +56 -0
- package/src/framework-scope.ts +49 -0
- package/src/generate-kinds.ts +97 -0
- package/src/guards.ts +186 -0
- package/src/index.ts +87 -14
- package/src/island-bundle.ts +166 -0
- package/src/island-routes.ts +50 -0
- package/src/jobs-driver.ts +33 -0
- package/src/jobs-json.ts +24 -0
- package/src/jobs-report.ts +17 -4
- package/src/mcp-db-target.ts +52 -27
- package/src/mcp-errors.ts +120 -19
- package/src/mcp-host.ts +44 -25
- package/src/messages.ts +81 -2
- package/src/metrics-endpoint.ts +73 -0
- package/src/migrations.ts +37 -4
- package/src/otlp-export.ts +64 -0
- package/src/output.ts +46 -16
- package/src/parse.ts +41 -3
- package/src/policy-facts.ts +38 -6
- package/src/policy-fixture.ts +14 -7
- package/src/prerender.ts +111 -2
- package/src/registry.ts +21 -3
- package/src/runtime-overrides.ts +66 -0
- package/src/safe-url-label.ts +24 -0
- package/src/scaffold-fixture.ts +10 -0
- package/src/scaffold-typecheck.ts +16 -38
- package/src/serve.ts +202 -18
- package/src/source-files.ts +4 -0
- package/src/statement-loop.ts +74 -0
- package/src/style-csp.ts +18 -0
- package/src/sync-authenticator.ts +59 -0
- package/src/templates/action.ts +15 -30
- package/src/templates/admin-page.ts +103 -0
- package/src/templates/admin.ts +11 -7
- package/src/templates/backfill.ts +212 -0
- package/src/templates/entity.ts +72 -31
- package/src/templates/guard.ts +143 -0
- package/src/templates/index.ts +12 -1
- package/src/templates/island.ts +67 -0
- package/src/templates/job.ts +53 -13
- package/src/templates/naming.ts +17 -1
- package/src/templates/policy.ts +35 -28
- package/src/templates/query.ts +24 -5
- package/src/templates/resource.ts +19 -11
- package/src/templates/route.ts +90 -15
- package/src/templates/scaffold-app.ts +142 -45
- package/src/templates/scaffold-claude-agents.ts +149 -0
- package/src/templates/scaffold-claude-commands.ts +221 -0
- package/src/templates/scaffold-claude.ts +134 -0
- package/src/templates/scaffold-container.ts +46 -2
- package/src/templates/scaffold-db-package.ts +91 -0
- package/src/templates/scaffold-docs.ts +24 -5
- package/src/templates/scaffold-domain-package.ts +90 -0
- package/src/templates/scaffold-env.ts +87 -0
- package/src/templates/scaffold-i18n.ts +4 -1
- package/src/templates/scaffold-mcp-package.ts +49 -0
- package/src/templates/scaffold-package-shape.ts +25 -4
- package/src/templates/scaffold-repo.ts +116 -257
- package/src/templates/scaffold-roles.ts +68 -0
- package/src/templates/scaffold-ui-package.ts +56 -0
- package/src/templates/slice-foundation.ts +88 -0
- package/src/templates/wrap.ts +95 -0
- package/src/test-counts.ts +35 -0
- package/src/test-select.ts +30 -15
- package/src/test-shards.ts +21 -3
- package/src/test-workers.ts +47 -0
- package/src/ts-scan.ts +271 -13
- package/src/tsconfig-references.ts +78 -0
- package/src/verify-floor.ts +133 -0
- package/src/verify-step.ts +19 -0
- package/src/verify-test-run.ts +72 -0
- package/src/verify-tests.ts +160 -71
- package/src/version-loader.ts +20 -3
- package/src/workspace-checks.ts +87 -16
- package/src/write-line.ts +34 -0
package/src/cmd-verify.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
+
import { renderThrowable } from '@ultimat3/core';
|
|
8
9
|
import type { Manifest } from '@ultimat3/manifest';
|
|
9
10
|
import {
|
|
10
11
|
AGENTS_MD_FILENAME,
|
|
@@ -14,18 +15,31 @@ import {
|
|
|
14
15
|
} from '@ultimat3/manifest';
|
|
15
16
|
import { checkAgentsMd } from './app-agents-md';
|
|
16
17
|
import { checkAppBoundaries } from './app-boundaries';
|
|
18
|
+
import { envExampleFindings } from './app-env';
|
|
17
19
|
import { appManifest, readAppManifest } from './app-manifest';
|
|
18
20
|
import { OPENAPI_FILE, openApiJson } from './app-openapi';
|
|
19
21
|
import { APP_CONFIG_FILE, requireAppRoot } from './app-root';
|
|
20
22
|
import { checkBudgets, readBuildStats } from './budgets';
|
|
21
23
|
import type { CliCommand, CommandContext } from './command';
|
|
22
|
-
import {
|
|
24
|
+
import { checkDestructiveMigrations } from './db-destructive';
|
|
25
|
+
import { checkDocumentStyles, documentSurfaces } from './document-styles';
|
|
26
|
+
import { checkSourceDrift } from './drift';
|
|
23
27
|
import { checkErrorFixes } from './error-contract';
|
|
28
|
+
import { readIntFlag } from './flag-number';
|
|
29
|
+
import { guardFindings } from './guards';
|
|
24
30
|
import { msg } from './messages';
|
|
25
31
|
import type { CommandResult, Finding, StepResult } from './output';
|
|
26
32
|
import { findingFrom } from './output';
|
|
33
|
+
import type { ParsedArgs } from './parse';
|
|
34
|
+
import {
|
|
35
|
+
floorProblemFindings,
|
|
36
|
+
floorRequires,
|
|
37
|
+
readVerifyFloor,
|
|
38
|
+
skippedSuiteFinding,
|
|
39
|
+
vanishedSuiteFinding,
|
|
40
|
+
} from './verify-floor';
|
|
27
41
|
import type { StepOutcome, VerifyContext, VerifyStep, VerifyStepName } from './verify-step';
|
|
28
|
-
import { fromExec, fromFindings, hostFindings
|
|
42
|
+
import { fromExec, fromFindings, hostFindings } from './verify-step';
|
|
29
43
|
import { TEST_STEPS } from './verify-tests';
|
|
30
44
|
import { checkFileSizes, checkPackageShape, hasWorkspacePackages } from './workspace-checks';
|
|
31
45
|
|
|
@@ -47,7 +61,11 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
47
61
|
},
|
|
48
62
|
{
|
|
49
63
|
name: 'lint',
|
|
50
|
-
|
|
64
|
+
// Only what biome actually enforces. It claimed "no default exports" while the rule was off
|
|
65
|
+
// (it is not in `recommended`) and "no raw colours" over a file type biome ignores entirely —
|
|
66
|
+
// two thirds of the line were enforced by nothing. `noDefaultExport` is now on in `biome.json`;
|
|
67
|
+
// the colour rule is `packages/ui/src/tokens/tokens.test.ts`, and rides on the `unit` step.
|
|
68
|
+
summary: 'biome: format, no any, no default exports, no unused imports',
|
|
51
69
|
async run(ctx) {
|
|
52
70
|
const result = await ctx.runner(['bunx', 'biome', 'check', '.'], { cwd: ctx.root });
|
|
53
71
|
return fromExec(result, {
|
|
@@ -59,10 +77,21 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
59
77
|
},
|
|
60
78
|
{
|
|
61
79
|
name: 'boundaries',
|
|
62
|
-
summary:
|
|
80
|
+
summary: "surface, layer and package-tier imports, and the app's own guards",
|
|
81
|
+
// An app's `guards/` rides here rather than becoming an eighteenth step, for the reason the
|
|
82
|
+
// seam already states: a host adds findings to a step, it can never add, remove, reorder or
|
|
83
|
+
// skip one — so "green" keeps meaning exactly what it meant. This is the step whose host slot
|
|
84
|
+
// already carries "rules this repo makes about itself that the framework cannot know" (the
|
|
85
|
+
// monorepo's tier table arrives through it), and it runs third, before any suite, so a
|
|
86
|
+
// convention failure is reported in seconds rather than after the tests.
|
|
87
|
+
//
|
|
88
|
+
// Discovered, not registered: `guardFindings` reads the directory. A guard that had to
|
|
89
|
+
// announce itself is a guard an app can forget to announce, which is the coupling axiom 8's
|
|
90
|
+
// extension model rejects.
|
|
63
91
|
run: async (ctx) =>
|
|
64
92
|
fromFindings([
|
|
65
93
|
...(await checkAppBoundaries(ctx.root)),
|
|
94
|
+
...(await guardFindings(ctx.root)),
|
|
66
95
|
...(await hostFindings(ctx, 'boundaries')),
|
|
67
96
|
]),
|
|
68
97
|
},
|
|
@@ -88,10 +117,20 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
88
117
|
...TEST_STEPS,
|
|
89
118
|
{
|
|
90
119
|
name: 'drift',
|
|
91
|
-
summary: 'schema vs migrations',
|
|
120
|
+
summary: 'schema source vs migrations, and every destructive statement declared',
|
|
92
121
|
// Only an app owns migrations; a package monorepo's `packages/db` is the driver, not a schema.
|
|
122
|
+
// Source, not database: the gate runs in CI with nothing listening, and the database half is
|
|
123
|
+
// the post-migrate verification `runMigrations` performs where a connection is already open.
|
|
124
|
+
//
|
|
125
|
+
// The destructive rail rides here rather than becoming an eighteenth step because it asks this
|
|
126
|
+
// step's own question — do the committed migrations still describe what the app is doing to its
|
|
127
|
+
// schema? — off the same directory, in the same pass, with no database either.
|
|
93
128
|
applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
|
|
94
|
-
run: async (ctx) =>
|
|
129
|
+
run: async (ctx) =>
|
|
130
|
+
fromFindings([
|
|
131
|
+
...(await checkSourceDrift(ctx.root)),
|
|
132
|
+
...(await checkDestructiveMigrations(ctx.root)),
|
|
133
|
+
]),
|
|
95
134
|
},
|
|
96
135
|
{
|
|
97
136
|
name: 'contract-diff',
|
|
@@ -112,24 +151,62 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
112
151
|
},
|
|
113
152
|
{
|
|
114
153
|
name: 'budgets',
|
|
115
|
-
summary: 'per-route JS bytes and LCP',
|
|
154
|
+
summary: 'per-route JS bytes and LCP, and the global style layer every document carries',
|
|
155
|
+
// The global-style assertion rides here rather than becoming an eighteenth step, because this
|
|
156
|
+
// step already asks the one question it asks: what does the document this build emits actually
|
|
157
|
+
// contain? It is also the same app load — `appManifest` fills render's stylesheet registry on
|
|
158
|
+
// its way through — so a separate step would pay for a second one to answer half a question.
|
|
159
|
+
//
|
|
160
|
+
// A repo with no `app.config.ts` is the framework monorepo, which renders no documents and has
|
|
161
|
+
// no stylesheet registry to read; there is nothing for either half to weigh.
|
|
162
|
+
applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
|
|
116
163
|
async run(ctx) {
|
|
164
|
+
// No stats file is NOT "nothing to weigh" — it is every declared budget unmeasured, which is
|
|
165
|
+
// the case `checkBudgets` already names per route (`X_BUDGET_UNMEASURED`). Skipping the half
|
|
166
|
+
// entirely is how a step that has never run once reported green: `.x/` is gitignored, so no
|
|
167
|
+
// CI run and neither gated app has ever had a `build-stats.json` for it to read.
|
|
168
|
+
//
|
|
169
|
+
// Handed over as `undefined` and never as `?? { routes: [] }`: "no build has run here" and
|
|
170
|
+
// "a build ran and could not weigh this route" are two different instructions, and only the
|
|
171
|
+
// caller knows which of them is true. The step still does not BUILD — measuring here would
|
|
172
|
+
// make `x verify` a static build on every run (8.2s on `dummy/social-media-clone`, and 5.9s
|
|
173
|
+
// to a hard `X_PRERENDER_FAILED` on `examples/dummy`), and it would be a second builder
|
|
174
|
+
// beside `apps/web/prerender.ts`, which is where an app reads `SITE_ORIGIN`.
|
|
117
175
|
const stats = await readBuildStats(ctx.root);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
176
|
+
// The load's own findings, FIRST and never dropped. A module that would not import registers
|
|
177
|
+
// no route, so its budget is missing from the manifest and every route it declared reads as
|
|
178
|
+
// `X_BUDGET_UNMEASURED` — the symptom, pointing the reader at `x build` for a file that will
|
|
179
|
+
// not compile. `contract-diff` reports these too when it applies; two red steps naming one
|
|
180
|
+
// broken module is honest, and one of them silently green over it is the false green.
|
|
181
|
+
const { manifest, findings } = await appManifest(ctx.root);
|
|
182
|
+
return fromFindings([
|
|
183
|
+
...findings,
|
|
184
|
+
...checkDocumentStyles(documentSurfaces()),
|
|
185
|
+
...checkBudgets(manifest, stats),
|
|
186
|
+
]);
|
|
121
187
|
},
|
|
122
188
|
},
|
|
123
189
|
{
|
|
124
190
|
name: 'manifest',
|
|
125
|
-
summary: 'the
|
|
191
|
+
summary: 'the files an agent reads: generated facts, hand-written conventions, the env example',
|
|
126
192
|
// No `applies`. The drift half has nothing to compare against until `x manifest` has run
|
|
127
193
|
// once, and says so by finding nothing — but `AGENTS.md` is required of every repo the gate
|
|
128
194
|
// runs in, so the step always has a question to answer and must never report as skipped.
|
|
195
|
+
//
|
|
196
|
+
// `.env.example` joins this step rather than becoming an eighteenth: the question is the same
|
|
197
|
+
// one — "does a committed, generated file still describe the code?" — and the step list is the
|
|
198
|
+
// definition of shippable, so it grows only when a genuinely new question needs asking.
|
|
199
|
+
//
|
|
200
|
+
// `x.verify.json` is here for that same question and no other: this step judges the floor
|
|
201
|
+
// FILE, `runVerify` judges the suites against it. A name the gate does not run can never
|
|
202
|
+
// vanish, so a typo in the floor covers nothing — which is the false green the floor exists to
|
|
203
|
+
// close, and it is only visible if something reads the file for its own sake.
|
|
129
204
|
async run(ctx) {
|
|
130
205
|
const agents = await checkAgentsMd(ctx.root);
|
|
131
206
|
const findings = [
|
|
132
207
|
...(await driftFindings(ctx.root)),
|
|
208
|
+
...(await envExampleFindings(ctx.root)),
|
|
209
|
+
...floorProblemFindings(await readVerifyFloor(ctx.root)),
|
|
133
210
|
...agents.findings,
|
|
134
211
|
...(await hostFindings(ctx, 'manifest')),
|
|
135
212
|
];
|
|
@@ -200,11 +277,24 @@ export async function runVerify(
|
|
|
200
277
|
steps: readonly VerifyStep[],
|
|
201
278
|
ctx: VerifyContext,
|
|
202
279
|
): Promise<CommandResult> {
|
|
280
|
+
const floor = await readVerifyFloor(ctx.root);
|
|
203
281
|
const results: StepResult[] = [];
|
|
204
282
|
for (const step of steps) {
|
|
205
283
|
const applies = step.applies === undefined ? true : await step.applies(ctx);
|
|
206
284
|
if (!applies) {
|
|
207
|
-
|
|
285
|
+
// A skip this repo already ruled out is not a skip. The step ran here before — the floor is
|
|
286
|
+
// that claim, committed — so "nothing to check" now means the suite was deleted, and the
|
|
287
|
+
// gate says so on the step's own line rather than counting one more thing not to worry
|
|
288
|
+
// about. Recorded as failed and NOT as skipped, so every reader of a step table sees it:
|
|
289
|
+
// the summary, `data.failed`, and the reference-app gate's own red list.
|
|
290
|
+
const required = floorRequires(floor, step.name);
|
|
291
|
+
results.push({
|
|
292
|
+
name: step.name,
|
|
293
|
+
ok: !required,
|
|
294
|
+
durationMs: 0,
|
|
295
|
+
skipped: !required,
|
|
296
|
+
findings: required ? [vanishedSuiteFinding(step.name)] : [],
|
|
297
|
+
});
|
|
208
298
|
continue;
|
|
209
299
|
}
|
|
210
300
|
const started = performance.now();
|
|
@@ -214,31 +304,71 @@ export async function runVerify(
|
|
|
214
304
|
findings: [findingOf(error, step.name)],
|
|
215
305
|
}),
|
|
216
306
|
);
|
|
307
|
+
// A step the floor requires whose suite executed nothing is the same vanished suite as a step
|
|
308
|
+
// with no files at all — the run just had to finish before it could be seen. Appended to the
|
|
309
|
+
// step's own findings so `data.failed`, the counts and every gate reading this table carry it.
|
|
310
|
+
const vanished =
|
|
311
|
+
floorRequires(floor, step.name) && outcome.tests !== undefined && outcome.tests.ran === 0
|
|
312
|
+
? [skippedSuiteFinding(step.name, outcome.tests.skipped)]
|
|
313
|
+
: [];
|
|
217
314
|
results.push({
|
|
218
315
|
name: step.name,
|
|
219
|
-
ok: outcome.ok,
|
|
316
|
+
ok: outcome.ok && vanished.length === 0,
|
|
220
317
|
durationMs: Math.round(performance.now() - started),
|
|
221
|
-
findings: outcome.findings,
|
|
318
|
+
findings: [...outcome.findings, ...vanished],
|
|
222
319
|
...(outcome.output === undefined ? {} : { output: outcome.output }),
|
|
320
|
+
...(outcome.workers === undefined ? {} : { workers: outcome.workers }),
|
|
223
321
|
});
|
|
224
322
|
}
|
|
225
323
|
const failedSteps = results.filter((step) => !step.ok).map((step) => step.name);
|
|
324
|
+
const skippedSteps = results.filter((step) => step.skipped === true).map((step) => step.name);
|
|
226
325
|
const totalMs = results.reduce((sum, step) => sum + step.durationMs, 0);
|
|
227
326
|
return {
|
|
228
327
|
ok: failedSteps.length === 0,
|
|
229
328
|
command: 'verify',
|
|
230
|
-
summary:
|
|
231
|
-
failedSteps.length === 0
|
|
232
|
-
? msg('cli.verify.pass', { count: results.length, ms: totalMs })
|
|
233
|
-
: msg('cli.verify.fail', { failed: failedSteps.length, count: results.length }),
|
|
329
|
+
summary: verifySummary({ results, failed: failedSteps, skipped: skippedSteps, totalMs }),
|
|
234
330
|
steps: results,
|
|
235
|
-
|
|
331
|
+
// `skipped` is a list beside `failed` and not a count, because the two answer the same kind of
|
|
332
|
+
// question — *which* steps, not how many — and a caller ratcheting on coverage needs the names.
|
|
333
|
+
data: { failed: failedSteps, skipped: skippedSteps, durationMs: totalMs },
|
|
236
334
|
exitCode: failedSteps.length === 0 ? 0 : 1,
|
|
237
335
|
};
|
|
238
336
|
}
|
|
239
337
|
|
|
338
|
+
/**
|
|
339
|
+
* What the counts are allowed to claim. A step that does not apply is recorded green so the run
|
|
340
|
+
* continues, and the summary counted it among the "all 17 steps passed" — so a repo whose `job`
|
|
341
|
+
* and `eval` suites do not exist reported the same line as a repo where both ran. `--json` carried
|
|
342
|
+
* the per-step flag all along; the one line every reader actually sees did not, which is how a
|
|
343
|
+
* vacuous gate stayed invisible. It names the skipped steps, not just how many: "17/17" is worth
|
|
344
|
+
* something only when the gap is visible in the same glance.
|
|
345
|
+
*/
|
|
346
|
+
function verifySummary(input: {
|
|
347
|
+
readonly results: readonly StepResult[];
|
|
348
|
+
readonly failed: readonly string[];
|
|
349
|
+
readonly skipped: readonly string[];
|
|
350
|
+
readonly totalMs: number;
|
|
351
|
+
}): string {
|
|
352
|
+
const params = {
|
|
353
|
+
count: input.results.length,
|
|
354
|
+
passed: input.results.filter((step) => step.ok && step.skipped !== true).length,
|
|
355
|
+
failed: input.failed.length,
|
|
356
|
+
skipped: input.skipped.length,
|
|
357
|
+
names: input.skipped.join(', '),
|
|
358
|
+
ms: input.totalMs,
|
|
359
|
+
};
|
|
360
|
+
const clean = input.skipped.length === 0;
|
|
361
|
+
if (input.failed.length === 0) {
|
|
362
|
+
return msg(clean ? 'cli.verify.pass' : 'cli.verify.passSkipped', params);
|
|
363
|
+
}
|
|
364
|
+
return msg(clean ? 'cli.verify.fail' : 'cli.verify.failSkipped', params);
|
|
365
|
+
}
|
|
366
|
+
|
|
240
367
|
function findingOf(error: unknown, step: string): Finding {
|
|
241
|
-
|
|
368
|
+
// A step may throw anything, including an Error that fights being read: `instanceof` runs a
|
|
369
|
+
// Proxy's `getPrototypeOf` trap and `.message` runs a getter, so a hostile throw would take the
|
|
370
|
+
// gate's own report down with it — the one message that may never be lost.
|
|
371
|
+
const cause = renderThrowable(error);
|
|
242
372
|
return {
|
|
243
373
|
code: 'X_VERIFY_FAILED',
|
|
244
374
|
cause: `step "${step}" threw: ${cause}`,
|
|
@@ -251,15 +381,39 @@ export const verifyCommand: CliCommand = {
|
|
|
251
381
|
spec: {
|
|
252
382
|
name: 'verify',
|
|
253
383
|
summary: 'the gate: typecheck, lint, boundaries, all tests, drift, contract, budgets',
|
|
254
|
-
usage: 'x verify [--json]',
|
|
384
|
+
usage: 'x verify [--workers N] [--json]',
|
|
255
385
|
requiresApp: true,
|
|
256
|
-
|
|
386
|
+
// The only flag, and it is not `--only`/`--skip` in disguise: it changes how wide the test
|
|
387
|
+
// steps spread, never which steps run. Every step still runs, so "green" still means the
|
|
388
|
+
// same thing at `--workers 1` as at `--workers 8`.
|
|
389
|
+
flags: [
|
|
390
|
+
{
|
|
391
|
+
name: 'workers',
|
|
392
|
+
type: 'string',
|
|
393
|
+
summary: 'test processes per parallel step (default: CPUs - 1, max 8)',
|
|
394
|
+
},
|
|
395
|
+
],
|
|
257
396
|
},
|
|
258
397
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
259
398
|
const root = requireAppRoot('verify', ctx.cwd).dir;
|
|
260
|
-
|
|
399
|
+
const workers = readWorkers(ctx.args);
|
|
400
|
+
return runVerify(VERIFY_STEPS, {
|
|
401
|
+
root,
|
|
402
|
+
runner: ctx.runner,
|
|
403
|
+
...(workers === undefined ? {} : { workers }),
|
|
404
|
+
});
|
|
261
405
|
},
|
|
262
406
|
};
|
|
263
407
|
|
|
408
|
+
/** `x test --workers` refuses the same values for the same reason — and now through the same
|
|
409
|
+
* reader, so the claim is enforced rather than asserted in a comment. */
|
|
410
|
+
const readWorkers = (args: ParsedArgs): number | undefined =>
|
|
411
|
+
readIntFlag(args, {
|
|
412
|
+
name: 'workers',
|
|
413
|
+
command: 'verify',
|
|
414
|
+
min: 1,
|
|
415
|
+
example: 'x verify --workers 4',
|
|
416
|
+
});
|
|
417
|
+
|
|
264
418
|
export const verifyStepNames = (): readonly VerifyStepName[] =>
|
|
265
419
|
VERIFY_STEPS.map((step) => step.name);
|