@ultimat3/cli 5.0.1 → 7.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.
Files changed (60) hide show
  1. package/CLAUDE.md +75 -6
  2. package/README.md +2 -2
  3. package/package.json +28 -24
  4. package/src/affected.ts +320 -0
  5. package/src/browser-launcher.ts +109 -0
  6. package/src/ci-log.ts +0 -0
  7. package/src/ci-runs.ts +179 -0
  8. package/src/cmd-affected.ts +109 -0
  9. package/src/cmd-build.ts +36 -3
  10. package/src/cmd-ci.ts +273 -0
  11. package/src/cmd-dev.ts +35 -2
  12. package/src/cmd-generate.ts +16 -348
  13. package/src/cmd-i18n.ts +32 -16
  14. package/src/cmd-pr.ts +308 -0
  15. package/src/cmd-shot.ts +320 -0
  16. package/src/cmd-test.ts +96 -7
  17. package/src/cmd-verify.ts +10 -427
  18. package/src/compile-externals.ts +34 -0
  19. package/src/dev-lock.ts +275 -0
  20. package/src/dev-render.ts +7 -17
  21. package/src/error-codes.ts +18 -0
  22. package/src/generate-files.ts +127 -0
  23. package/src/generate-write.ts +229 -0
  24. package/src/gh-target.ts +118 -0
  25. package/src/gh.ts +204 -0
  26. package/src/i18n-audit.ts +39 -1
  27. package/src/i18n-registration.ts +130 -0
  28. package/src/index.ts +37 -0
  29. package/src/island-bundle.ts +68 -2
  30. package/src/island-solid-production.ts +129 -0
  31. package/src/island-styles.ts +41 -0
  32. package/src/mcp-errors.ts +11 -0
  33. package/src/messages.ts +67 -0
  34. package/src/pr-threads.ts +291 -0
  35. package/src/prerender.ts +52 -10
  36. package/src/registry.ts +8 -0
  37. package/src/shot-verdict.ts +337 -0
  38. package/src/solid-loader.ts +127 -0
  39. package/src/static-report.ts +219 -0
  40. package/src/templates/admin-page.ts +46 -5
  41. package/src/templates/index.ts +1 -0
  42. package/src/templates/island-fixture.ts +76 -0
  43. package/src/templates/island.ts +129 -18
  44. package/src/templates/resource-form-island.ts +279 -0
  45. package/src/templates/resource.ts +52 -43
  46. package/src/templates/route.ts +45 -6
  47. package/src/templates/scaffold-app.ts +70 -19
  48. package/src/templates/scaffold-container.ts +2 -2
  49. package/src/templates/scaffold-db-package.ts +88 -39
  50. package/src/templates/scaffold-docs.ts +18 -1
  51. package/src/templates/scaffold-i18n.ts +9 -2
  52. package/src/templates/scaffold-mcp-package.ts +35 -2
  53. package/src/templates/scaffold-package-shape.ts +7 -2
  54. package/src/templates/scaffold-repo.ts +2 -2
  55. package/src/test-shards.ts +19 -3
  56. package/src/verify-checks.ts +349 -0
  57. package/src/verify-run.ts +122 -0
  58. package/src/verify-step.ts +7 -0
  59. package/src/workspace-graph.ts +241 -0
  60. package/types/babel-modules.d.ts +31 -0
@@ -0,0 +1,349 @@
1
+ // What each named gate step actually checks, in cost order. Split from `cmd-verify.ts`, which is
2
+ // now the `x verify` command surface alone: the list is the definition of shippable and grows with
3
+ // the framework, while the command that runs it does not.
4
+
5
+ import { existsSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import type { Manifest } from '@ultimat3/manifest';
8
+ import {
9
+ AGENTS_MD_FILENAME,
10
+ assertNoDrift,
11
+ MANIFEST_FILENAME,
12
+ verifyContract,
13
+ } from '@ultimat3/manifest';
14
+ import type { MetaIssue } from '@ultimat3/seo';
15
+ import { validateMeta } from '@ultimat3/seo';
16
+ import { checkAgentsMd } from './app-agents-md';
17
+ import { checkAppBoundaries } from './app-boundaries';
18
+ import { envExampleFindings } from './app-env';
19
+ import { appManifest, readAppManifest } from './app-manifest';
20
+ import { OPENAPI_FILE, openApiJson } from './app-openapi';
21
+ import { APP_CONFIG_FILE } from './app-root';
22
+ import { checkBudgets, readBuildStats } from './budgets';
23
+ import { checkDestructiveMigrations } from './db-destructive';
24
+ import { checkDocumentStyles, documentSurfaces } from './document-styles';
25
+ import { checkSourceDrift } from './drift';
26
+ import { checkErrorFixReport } from './error-contract';
27
+ import { guardFindings } from './guards';
28
+ import { catalogFindings } from './i18n-registration';
29
+ import { msg } from './messages';
30
+ import type { Finding } from './output';
31
+ import { findingFrom } from './output';
32
+ import { scanSiteMeta } from './seo-meta';
33
+ import { floorProblemFindings, readVerifyFloor } from './verify-floor';
34
+ import type { VerifyStep } from './verify-step';
35
+ import { fromExec, fromFindings, hostFindings } from './verify-step';
36
+ import { TEST_STEPS } from './verify-tests';
37
+ import { checkFileSizes, checkPackageShape, hasWorkspacePackages } from './workspace-checks';
38
+ import { checkWorkspaceDependencies } from './workspace-graph';
39
+
40
+ /** The one file that makes the `roadmap` step answerable, and therefore what `applies` reads. */
41
+ const ROADMAP_FILE = join('docs', 'idea', '14-roadmap.md');
42
+
43
+ /** The whole contract, in cost order. Every check the framework knows how to make lives here. */
44
+ export const VERIFY_STEPS: readonly VerifyStep[] = [
45
+ {
46
+ name: 'typecheck',
47
+ summary: 'tsc -b across every project the root references',
48
+ async run(ctx) {
49
+ const result = await ctx.runner(['bunx', 'tsc', '-b', '--pretty', 'false'], {
50
+ cwd: ctx.root,
51
+ });
52
+ return fromExec(result, {
53
+ code: 'X_TYPECHECK_FAILED',
54
+ cause: 'the project does not typecheck',
55
+ fix: 'bunx tsc -b --pretty false',
56
+ });
57
+ },
58
+ },
59
+ {
60
+ name: 'lint',
61
+ // Only what biome actually enforces. It claimed "no default exports" while the rule was off
62
+ // (it is not in `recommended`) and "no raw colours" over a file type biome ignores entirely —
63
+ // two thirds of the line were enforced by nothing. `noDefaultExport` is now on in `biome.json`;
64
+ // the colour rule is `packages/ui/src/tokens/tokens.test.ts`, and rides on the `unit` step.
65
+ summary: 'biome: format, no any, no default exports, no unused imports',
66
+ async run(ctx) {
67
+ const result = await ctx.runner(['bunx', 'biome', 'check', '.'], { cwd: ctx.root });
68
+ return fromExec(result, {
69
+ code: 'X_LINT_FAILED',
70
+ cause: 'biome reported problems',
71
+ fix: 'bunx biome check --write .',
72
+ });
73
+ },
74
+ },
75
+ {
76
+ name: 'boundaries',
77
+ summary: "surface, layer and package-tier imports, and the app's own guards",
78
+ // An app's `guards/` rides here rather than becoming an eighteenth step, for the reason the
79
+ // seam already states: a host adds findings to a step, it can never add, remove, reorder or
80
+ // skip one — so "green" keeps meaning exactly what it meant. This is the step whose host slot
81
+ // already carries "rules this repo makes about itself that the framework cannot know" (the
82
+ // monorepo's tier table arrives through it), and it runs third, before any suite, so a
83
+ // convention failure is reported in seconds rather than after the tests.
84
+ //
85
+ // Discovered, not registered: `guardFindings` reads the directory. A guard that had to
86
+ // announce itself is a guard an app can forget to announce, which is the coupling axiom 8's
87
+ // extension model rejects.
88
+ run: async (ctx) =>
89
+ fromFindings([
90
+ ...(await checkAppBoundaries(ctx.root)),
91
+ ...(await guardFindings(ctx.root)),
92
+ ...(await hostFindings(ctx, 'boundaries')),
93
+ ]),
94
+ },
95
+ {
96
+ name: 'filesize',
97
+ summary: 'one file, one job',
98
+ run: async (ctx) => fromFindings(await checkFileSizes(ctx.root)),
99
+ },
100
+ {
101
+ name: 'package-shape',
102
+ summary: 'every package ships the same contract files',
103
+ applies: (ctx) => hasWorkspacePackages(ctx.root),
104
+ // The dependency rule rides here rather than becoming a twentieth step because it is this
105
+ // step's own question — what does a workspace owe the repo it lives in? — asked of the
106
+ // manifest's `dependencies` instead of its `files`. It is deliberately NOT inside
107
+ // `checkPackageShape`: `scripts/release.ts --check` calls that one to ask whether the tree is
108
+ // at the version a tag claims, and an undeclared import is not that question.
109
+ run: async (ctx) =>
110
+ fromFindings([
111
+ ...(await checkPackageShape(ctx.root)),
112
+ ...(await checkWorkspaceDependencies(ctx.root)),
113
+ ]),
114
+ },
115
+ {
116
+ name: 'errors',
117
+ summary: 'every X_* code has a runnable fix and a docs page',
118
+ // The fix-line half runs anywhere source does. The docs half needs a reference page to check
119
+ // against, and which file that is belongs to the host repo — hence `hostFindings`.
120
+ //
121
+ // The coverage line rides in `output`, which `--json` carries verbatim: a scan without a
122
+ // parser cannot read every fix, and a step that reports only findings claims a completeness
123
+ // it does not have. "checked 412, could not read 27" is what a reader can act on.
124
+ async run(ctx) {
125
+ const report = await checkErrorFixReport(ctx.root);
126
+ const findings = [...report.findings, ...(await hostFindings(ctx, 'errors'))];
127
+ return {
128
+ ...fromFindings(findings),
129
+ output: msg('cli.verify.fixCoverage', {
130
+ checked: report.checked,
131
+ unreadable: report.unreadable,
132
+ }),
133
+ };
134
+ },
135
+ },
136
+ ...TEST_STEPS,
137
+ {
138
+ name: 'drift',
139
+ summary: 'schema source vs migrations, and every destructive statement declared',
140
+ // Only an app owns migrations; a package monorepo's `packages/db` is the driver, not a schema.
141
+ // Source, not database: the gate runs in CI with nothing listening, and the database half is
142
+ // the post-migrate verification `runMigrations` performs where a connection is already open.
143
+ //
144
+ // The destructive rail rides here rather than becoming an eighteenth step because it asks this
145
+ // step's own question — do the committed migrations still describe what the app is doing to its
146
+ // schema? — off the same directory, in the same pass, with no database either.
147
+ applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
148
+ run: async (ctx) =>
149
+ fromFindings([
150
+ ...(await checkSourceDrift(ctx.root)),
151
+ ...(await checkDestructiveMigrations(ctx.root)),
152
+ ]),
153
+ },
154
+ {
155
+ name: 'contract-diff',
156
+ summary: 'the published contract vs the committed manifest',
157
+ // Either file is a published contract on its own: `openapi.json` generates the typed client,
158
+ // so gating on the manifest alone let a stale spec ship a wrong client unchecked.
159
+ applies: async (ctx) =>
160
+ existsSync(join(ctx.root, MANIFEST_FILENAME)) || existsSync(join(ctx.root, OPENAPI_FILE)),
161
+ async run(ctx) {
162
+ const committed = await readAppManifest(ctx.root);
163
+ const { manifest, findings } = await appManifest(ctx.root);
164
+ return fromFindings([
165
+ ...findings,
166
+ ...(committed === undefined ? [] : contractFindings(committed, manifest)),
167
+ ...(await specFindings(ctx.root, manifest)),
168
+ ]);
169
+ },
170
+ },
171
+ {
172
+ name: 'budgets',
173
+ summary: 'per-route JS bytes and LCP, and the global style layer every document carries',
174
+ // The global-style assertion rides here rather than becoming an eighteenth step, because this
175
+ // step already asks the one question it asks: what does the document this build emits actually
176
+ // contain? It is also the same app load — `appManifest` fills render's stylesheet registry on
177
+ // its way through — so a separate step would pay for a second one to answer half a question.
178
+ //
179
+ // A repo with no `app.config.ts` is the framework monorepo, which renders no documents and has
180
+ // no stylesheet registry to read; there is nothing for either half to weigh.
181
+ applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
182
+ async run(ctx) {
183
+ // No stats file is NOT "nothing to weigh" — it is every declared budget unmeasured, which is
184
+ // the case `checkBudgets` already names per route (`X_BUDGET_UNMEASURED`). Skipping the half
185
+ // entirely is how a step that has never run once reported green: `.x/` is gitignored, so no
186
+ // CI run and neither gated app has ever had a `build-stats.json` for it to read.
187
+ //
188
+ // Handed over as `undefined` and never as `?? { routes: [] }`: "no build has run here" and
189
+ // "a build ran and could not weigh this route" are two different instructions, and only the
190
+ // caller knows which of them is true. The step still does not BUILD — measuring here would
191
+ // make `x verify` a static build on every run (8.2s on `dummy/social-media-clone`, and 5.9s
192
+ // to a hard `X_PRERENDER_FAILED` on `examples/dummy`), and it would be a second builder
193
+ // beside `apps/web/prerender.ts`, which is where an app reads `SITE_ORIGIN`.
194
+ const stats = await readBuildStats(ctx.root);
195
+ // The load's own findings, FIRST and never dropped. A module that would not import registers
196
+ // no route, so its budget is missing from the manifest and every route it declared reads as
197
+ // `X_BUDGET_UNMEASURED` — the symptom, pointing the reader at `x build` for a file that will
198
+ // not compile. `contract-diff` reports these too when it applies; two red steps naming one
199
+ // broken module is honest, and one of them silently green over it is the false green.
200
+ const { manifest, findings } = await appManifest(ctx.root);
201
+ return fromFindings([
202
+ ...findings,
203
+ ...checkDocumentStyles(documentSurfaces()),
204
+ ...checkBudgets(manifest, stats),
205
+ ]);
206
+ },
207
+ },
208
+ {
209
+ name: 'seo',
210
+ summary: 'every indexable site/ route has a title and a description a search result can render',
211
+ // The SEO checkers shipped in `@ultimat3/seo` with no caller anywhere — `validateMeta` and its
212
+ // asserts were reachable only by an app that called them itself, which is what
213
+ // `packages/seo/src/errors.ts`'s own header said. This is the caller.
214
+ //
215
+ // Its own step rather than a rider on `budgets`: that step asks what a document WEIGHS and
216
+ // this one asks what it SAYS, and a missing `<title>` reported under `budgets` would hand the
217
+ // reader a fix for the wrong question (axiom 4). It costs no second app load — `loadApp`
218
+ // imports each module once per process, so this runs on the registries `budgets` just filled.
219
+ applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
220
+ async run(ctx) {
221
+ const scan = await scanSiteMeta(ctx.root);
222
+ // No `baseUrl`: an app declares no base URL anywhere (`packages/core/src/config.ts` has no
223
+ // such key), so canonical checks are skipped rather than run against an origin this file
224
+ // invented. `seo-meta.ts` spells out why that is the honest half.
225
+ const report = validateMeta(scan.records);
226
+ return {
227
+ ok: scan.findings.length === 0 && report.ok,
228
+ findings: [...scan.findings, ...report.issues.map(seoFinding)],
229
+ };
230
+ },
231
+ },
232
+ {
233
+ name: 'i18n',
234
+ summary: 'every string this app renders resolves — in the catalogs AND in the registry',
235
+ // Its own step rather than a rider on `boundaries`, by that step's own test: a rider must ask
236
+ // the SAME question off the same data, and "did this import cross a line?" is not "did this
237
+ // declaration reach the running app?". Reported under `boundaries`, `X_CATALOG_UNREGISTERED`
238
+ // would send a reader to the import graph for a bug that lives in the boot path (axiom 4).
239
+ //
240
+ // Both halves, because they are one question and both ship the same `⟦key⟧` to a user: a key
241
+ // missing from a locale's catalog, and a catalog no module ever registered. `x i18n check`
242
+ // reports exactly these findings from exactly this call, so the command and the gate cannot
243
+ // disagree about an app.
244
+ //
245
+ // Beside `seo` and after `budgets` for the reason `seo` gives: `loadApp` imports each module
246
+ // once per process, so this runs on the registries `budgets` just filled and pays for no
247
+ // second load. A repo with no `app.config.ts` is the framework monorepo, which registers no
248
+ // app catalogs — SKIPPED there, never passed, because a step that answers `ok` about nothing
249
+ // is the vacuous green this check exists to refuse.
250
+ applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
251
+ run: async (ctx) => fromFindings(await catalogFindings(ctx.root)),
252
+ },
253
+ {
254
+ name: 'manifest',
255
+ summary: 'the files an agent reads: generated facts, hand-written conventions, the env example',
256
+ // No `applies`. The drift half has nothing to compare against until `x manifest` has run
257
+ // once, and says so by finding nothing — but `AGENTS.md` is required of every repo the gate
258
+ // runs in, so the step always has a question to answer and must never report as skipped.
259
+ //
260
+ // `.env.example` joins this step rather than becoming an eighteenth: the question is the same
261
+ // one — "does a committed, generated file still describe the code?" — and the step list is the
262
+ // definition of shippable, so it grows only when a genuinely new question needs asking.
263
+ //
264
+ // `x.verify.json` is here for that same question and no other: this step judges the floor
265
+ // FILE, `runVerify` judges the suites against it. A name the gate does not run can never
266
+ // vanish, so a typo in the floor covers nothing — which is the false green the floor exists to
267
+ // close, and it is only visible if something reads the file for its own sake.
268
+ async run(ctx) {
269
+ const agents = await checkAgentsMd(ctx.root);
270
+ const findings = [
271
+ ...(await driftFindings(ctx.root)),
272
+ ...(await envExampleFindings(ctx.root)),
273
+ ...floorProblemFindings(await readVerifyFloor(ctx.root)),
274
+ ...agents.findings,
275
+ ...(await hostFindings(ctx, 'manifest')),
276
+ ];
277
+ // Warnings are not findings: `AGENTS.md` tabulating a route table is a smell a human
278
+ // judges, not a build error. They ride in `output`, which `--json` carries verbatim.
279
+ const output = agents.warnings.map((warning) => `${AGENTS_MD_FILENAME}: ${warning}`);
280
+ return {
281
+ ok: findings.length === 0,
282
+ findings,
283
+ ...(output.length === 0 ? {} : { output: output.join('\n') }),
284
+ };
285
+ },
286
+ },
287
+ {
288
+ name: 'roadmap',
289
+ summary: "every roadmap milestone's status marker matches what is actually on disk",
290
+ // A generated app ships no `docs/idea/14-roadmap.md` — only the framework monorepo does, so
291
+ // the FILE is what decides. It keyed on `ctx.hostChecks?.roadmap` until `As of 2026-08`, which
292
+ // is a fact about the CALL: a caller of the exported `runVerify(VERIFY_STEPS, ctx)` passing no
293
+ // `hostChecks`, in a repo whose committed `x.verify.json` names `roadmap`, got
294
+ // `X_VERIFY_SUITE_VANISHED` — whose `fix:` is the command that had just failed.
295
+ applies: async (ctx) => existsSync(join(ctx.root, ROADMAP_FILE)),
296
+ run: async (ctx) => fromFindings(await hostFindings(ctx, 'roadmap')),
297
+ },
298
+ ];
299
+
300
+ /** `assertNoDrift` throws `X_MANIFEST_DRIFT`; a step reports, so the error becomes a finding. */
301
+ async function driftFindings(root: string): Promise<readonly Finding[]> {
302
+ const path = join(root, MANIFEST_FILENAME);
303
+ if (!existsSync(path)) return [];
304
+ const { manifest, findings } = await appManifest(root);
305
+ try {
306
+ await assertNoDrift({ manifest, path });
307
+ return findings;
308
+ } catch (error) {
309
+ return [...findings, { ...findingFrom(error), at: MANIFEST_FILENAME }];
310
+ }
311
+ }
312
+
313
+ /** A breaking change is allowed — with a major bump. `verifyContract` is the one that decides. */
314
+ function contractFindings(before: Manifest, after: Manifest): readonly Finding[] {
315
+ try {
316
+ verifyContract({ before, after });
317
+ return [];
318
+ } catch (error) {
319
+ return [{ ...findingFrom(error), at: MANIFEST_FILENAME }];
320
+ }
321
+ }
322
+
323
+ /** The typed client is generated from `openapi.json`, so a stale spec ships a wrong client. */
324
+ async function specFindings(root: string, manifest: Manifest): Promise<readonly Finding[]> {
325
+ const path = join(root, OPENAPI_FILE);
326
+ if (!existsSync(path)) return [];
327
+ if ((await Bun.file(path).text()) === openApiJson(manifest)) return [];
328
+ return [
329
+ {
330
+ code: 'X_MANIFEST_STALE',
331
+ cause: `${OPENAPI_FILE} does not match the actions the code registers`,
332
+ fix: 'x manifest',
333
+ docs: 'https://ultimate.dev/errors/X_MANIFEST_STALE',
334
+ at: OPENAPI_FILE,
335
+ },
336
+ ];
337
+ }
338
+
339
+ /**
340
+ * One `MetaIssue` as the gate reports it. `at` is the route FILE and never the URL: every seo error
341
+ * already names the file in its cause, and `at` is what an agent opens.
342
+ */
343
+ const seoFinding = (issue: MetaIssue): Finding => ({
344
+ code: issue.code,
345
+ cause: issue.cause,
346
+ fix: issue.fix,
347
+ docs: `https://ultimate.dev/errors/${issue.code}`,
348
+ at: issue.file,
349
+ });
@@ -0,0 +1,122 @@
1
+ // Run a step list end to end and turn it into the one table every reader sees. Split from
2
+ // `cmd-verify.ts` because `x build` and the MCP host run the gate without going through the
3
+ // command: what a run means — never bail early, count what actually ran — belongs to neither.
4
+
5
+ import { renderThrowable } from '@ultimat3/core';
6
+ import { msg } from './messages';
7
+ import type { CommandResult, Finding, StepResult } from './output';
8
+ import {
9
+ floorRequires,
10
+ readVerifyFloor,
11
+ skippedSuiteFinding,
12
+ vanishedSuiteFinding,
13
+ } from './verify-floor';
14
+ import type { StepOutcome, VerifyContext, VerifyStep } from './verify-step';
15
+
16
+ /**
17
+ * Run every step in order, never bailing early: an agent fixing three things at once needs all
18
+ * three findings from one run, not one per round-trip.
19
+ */
20
+ export async function runVerify(
21
+ steps: readonly VerifyStep[],
22
+ ctx: VerifyContext,
23
+ ): Promise<CommandResult> {
24
+ const floor = await readVerifyFloor(ctx.root);
25
+ const results: StepResult[] = [];
26
+ for (const step of steps) {
27
+ const applies = step.applies === undefined ? true : await step.applies(ctx);
28
+ if (!applies) {
29
+ // A skip this repo already ruled out is not a skip. The step ran here before — the floor is
30
+ // that claim, committed — so "nothing to check" now means the suite was deleted, and the
31
+ // gate says so on the step's own line rather than counting one more thing not to worry
32
+ // about. Recorded as failed and NOT as skipped, so every reader of a step table sees it:
33
+ // the summary, `data.failed`, and the reference-app gate's own red list.
34
+ const required = floorRequires(floor, step.name);
35
+ results.push({
36
+ name: step.name,
37
+ ok: !required,
38
+ durationMs: 0,
39
+ skipped: !required,
40
+ findings: required ? [vanishedSuiteFinding(step.name)] : [],
41
+ });
42
+ continue;
43
+ }
44
+ const started = performance.now();
45
+ const outcome = await step.run(ctx).catch(
46
+ (error: unknown): StepOutcome => ({
47
+ ok: false,
48
+ findings: [findingOf(error, step.name)],
49
+ }),
50
+ );
51
+ // A step the floor requires whose suite executed nothing is the same vanished suite as a step
52
+ // with no files at all — the run just had to finish before it could be seen. Appended to the
53
+ // step's own findings so `data.failed`, the counts and every gate reading this table carry it.
54
+ const vanished =
55
+ floorRequires(floor, step.name) && outcome.tests !== undefined && outcome.tests.ran === 0
56
+ ? [skippedSuiteFinding(step.name, outcome.tests.skipped)]
57
+ : [];
58
+ results.push({
59
+ name: step.name,
60
+ ok: outcome.ok && vanished.length === 0,
61
+ durationMs: Math.round(performance.now() - started),
62
+ findings: [...outcome.findings, ...vanished],
63
+ ...(outcome.output === undefined ? {} : { output: outcome.output }),
64
+ ...(outcome.workers === undefined ? {} : { workers: outcome.workers }),
65
+ });
66
+ }
67
+ const failedSteps = results.filter((step) => !step.ok).map((step) => step.name);
68
+ const skippedSteps = results.filter((step) => step.skipped === true).map((step) => step.name);
69
+ const totalMs = results.reduce((sum, step) => sum + step.durationMs, 0);
70
+ return {
71
+ ok: failedSteps.length === 0,
72
+ command: 'verify',
73
+ summary: verifySummary({ results, failed: failedSteps, skipped: skippedSteps, totalMs }),
74
+ steps: results,
75
+ // `skipped` is a list beside `failed` and not a count, because the two answer the same kind of
76
+ // question — *which* steps, not how many — and a caller ratcheting on coverage needs the names.
77
+ data: { failed: failedSteps, skipped: skippedSteps, durationMs: totalMs },
78
+ exitCode: failedSteps.length === 0 ? 0 : 1,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * What the counts are allowed to claim. A step that does not apply is recorded green so the run
84
+ * continues, and the summary counted it among the "all 17 steps passed" — so a repo whose `job`
85
+ * and `eval` suites do not exist reported the same line as a repo where both ran. `--json` carried
86
+ * the per-step flag all along; the one line every reader actually sees did not, which is how a
87
+ * vacuous gate stayed invisible. It names the skipped steps, not just how many: "17/17" is worth
88
+ * something only when the gap is visible in the same glance.
89
+ */
90
+ function verifySummary(input: {
91
+ readonly results: readonly StepResult[];
92
+ readonly failed: readonly string[];
93
+ readonly skipped: readonly string[];
94
+ readonly totalMs: number;
95
+ }): string {
96
+ const params = {
97
+ count: input.results.length,
98
+ passed: input.results.filter((step) => step.ok && step.skipped !== true).length,
99
+ failed: input.failed.length,
100
+ skipped: input.skipped.length,
101
+ names: input.skipped.join(', '),
102
+ ms: input.totalMs,
103
+ };
104
+ const clean = input.skipped.length === 0;
105
+ if (input.failed.length === 0) {
106
+ return msg(clean ? 'cli.verify.pass' : 'cli.verify.passSkipped', params);
107
+ }
108
+ return msg(clean ? 'cli.verify.fail' : 'cli.verify.failSkipped', params);
109
+ }
110
+
111
+ function findingOf(error: unknown, step: string): Finding {
112
+ // A step may throw anything, including an Error that fights being read: `instanceof` runs a
113
+ // Proxy's `getPrototypeOf` trap and `.message` runs a getter, so a hostile throw would take the
114
+ // gate's own report down with it — the one message that may never be lost.
115
+ const cause = renderThrowable(error);
116
+ return {
117
+ code: 'X_VERIFY_FAILED',
118
+ cause: `step "${step}" threw: ${cause}`,
119
+ fix: 'x verify --json',
120
+ docs: 'https://ultimate.dev/errors/X_VERIFY_FAILED',
121
+ };
122
+ }
@@ -33,6 +33,13 @@ export const VERIFY_STEP_NAMES = [
33
33
  // enforces — and `verifyCommand.run` passes no host checks at all, so the app path could not
34
34
  // have carried it. It runs beside `budgets` because both read the app the same load produced.
35
35
  'seo',
36
+ // Nineteenth, by the same test the SEO step above passed and for the same reason it is not a
37
+ // rider: `boundaries` asks whether an import was LEGAL and this asks whether a declaration
38
+ // REACHED the running app, which is a different question with a different fix (axiom 4). It
39
+ // costs no second app load — `budgets` already imported every module, and this reads the
40
+ // registries that load filled. Until it existed, an app could ship every user-facing string as
41
+ // `⟦key⟧` with `x verify` green, because nothing in the gate ever asked (issue #249).
42
+ 'i18n',
36
43
  'manifest',
37
44
  'roadmap',
38
45
  ] as const;