@ultimat3/cli 21.0.0 → 22.1.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 (236) hide show
  1. package/CLAUDE.md +84 -1650
  2. package/README.md +40 -2
  3. package/package.json +30 -30
  4. package/src/api-registration.ts +124 -0
  5. package/src/app-artifacts.ts +39 -0
  6. package/src/app-boundaries.ts +5 -10
  7. package/src/app-load.ts +37 -5
  8. package/src/app-openapi.ts +27 -0
  9. package/src/app-permissions.ts +0 -0
  10. package/src/app-root.ts +1 -1
  11. package/src/async-pages.ts +32 -0
  12. package/src/bin.ts +7 -1
  13. package/src/boundary-findings.ts +36 -0
  14. package/src/browser-launcher-fake-html.ts +80 -0
  15. package/src/browser-launcher-fake.ts +165 -0
  16. package/src/browser-launcher-port.ts +159 -0
  17. package/src/browser-launcher.ts +26 -133
  18. package/src/budgets.ts +11 -1
  19. package/src/cdp-shot-a11y.ts +77 -0
  20. package/src/cdp-shot-clock.ts +14 -0
  21. package/src/cdp-shot-driver.ts +150 -0
  22. package/src/cdp-shot-element.ts +147 -0
  23. package/src/cdp-shot-errors.ts +62 -0
  24. package/src/cdp-shot-keys.ts +152 -0
  25. package/src/cdp-shot-page.ts +230 -0
  26. package/src/cdp-shot-watch.ts +241 -0
  27. package/src/cmd-affected-spec.ts +24 -0
  28. package/src/cmd-affected.ts +3 -19
  29. package/src/cmd-build-spec.ts +16 -0
  30. package/src/cmd-build.ts +46 -20
  31. package/src/cmd-ci-spec.ts +24 -0
  32. package/src/cmd-ci.ts +17 -21
  33. package/src/cmd-db-branch.ts +2 -2
  34. package/src/cmd-db-spec.ts +99 -0
  35. package/src/cmd-db.ts +15 -94
  36. package/src/cmd-deploy-helm.ts +136 -0
  37. package/src/cmd-deploy-spec.ts +40 -0
  38. package/src/cmd-deploy.ts +111 -43
  39. package/src/cmd-dev-spec.ts +25 -0
  40. package/src/cmd-dev.ts +55 -36
  41. package/src/cmd-docs-spec.ts +16 -0
  42. package/src/cmd-docs.ts +2 -11
  43. package/src/cmd-doctor-spec.ts +21 -0
  44. package/src/cmd-doctor.ts +4 -18
  45. package/src/cmd-env-spec.ts +18 -0
  46. package/src/cmd-env.ts +4 -13
  47. package/src/cmd-errors-spec.ts +23 -0
  48. package/src/cmd-errors.ts +3 -17
  49. package/src/cmd-fix-spec.ts +18 -0
  50. package/src/cmd-fix.ts +4 -13
  51. package/src/cmd-generate-spec.ts +31 -0
  52. package/src/cmd-generate.ts +46 -42
  53. package/src/cmd-i18n-spec.ts +16 -0
  54. package/src/cmd-i18n.ts +7 -14
  55. package/src/cmd-jobs-spec.ts +56 -0
  56. package/src/cmd-jobs.ts +3 -50
  57. package/src/cmd-manifest-spec.ts +15 -0
  58. package/src/cmd-manifest.ts +19 -20
  59. package/src/cmd-mcp-spec.ts +21 -0
  60. package/src/cmd-mcp.ts +2 -15
  61. package/src/cmd-new-spec.ts +33 -0
  62. package/src/cmd-new.ts +38 -33
  63. package/src/cmd-policy-spec.ts +13 -0
  64. package/src/cmd-policy.ts +2 -9
  65. package/src/cmd-pr-spec.ts +39 -0
  66. package/src/cmd-pr.ts +5 -65
  67. package/src/cmd-registries-spec.ts +32 -0
  68. package/src/cmd-registries.ts +4 -25
  69. package/src/cmd-routes-spec.ts +12 -0
  70. package/src/cmd-routes.ts +2 -7
  71. package/src/cmd-secrets-spec.ts +19 -0
  72. package/src/cmd-secrets.ts +22 -34
  73. package/src/cmd-shot-island.ts +12 -13
  74. package/src/cmd-shot-spec.ts +53 -0
  75. package/src/cmd-shot.ts +11 -60
  76. package/src/cmd-tasks-spec.ts +21 -0
  77. package/src/cmd-tasks.ts +2 -16
  78. package/src/cmd-test-spec.ts +54 -0
  79. package/src/cmd-test.ts +4 -49
  80. package/src/cmd-verify-spec.ts +28 -0
  81. package/src/cmd-verify.ts +3 -23
  82. package/src/db-seed.ts +4 -2
  83. package/src/dev-dashboard.ts +4 -4
  84. package/src/dev-lock.ts +18 -2
  85. package/src/dev-port.ts +21 -0
  86. package/src/dev-route-table.ts +3 -3
  87. package/src/dispatch.ts +12 -5
  88. package/src/document-styles.ts +1 -1
  89. package/src/drift.ts +4 -18
  90. package/src/error-catalog.ts +16 -9
  91. package/src/error-codes.ts +28 -30
  92. package/src/error-pages.ts +19 -7
  93. package/src/errors.ts +14 -0
  94. package/src/favicon.ts +2 -2
  95. package/src/fix-imports.ts +1 -1
  96. package/src/fix-scan.ts +2 -9
  97. package/src/flag-reads.ts +1 -2
  98. package/src/foreign-text.ts +36 -0
  99. package/src/framework-schema.ts +5 -6
  100. package/src/generate-feature.ts +42 -0
  101. package/src/generate-files.ts +7 -3
  102. package/src/generate-grants.ts +83 -0
  103. package/src/generate-kinds.ts +56 -4
  104. package/src/i18n-index.ts +59 -10
  105. package/src/icon-assets.ts +1 -1
  106. package/src/image-prepare.ts +14 -0
  107. package/src/import-scan.ts +63 -0
  108. package/src/index.ts +67 -348
  109. package/src/invocation-flags.ts +26 -0
  110. package/src/island-bundle.ts +6 -1
  111. package/src/island-capture.ts +6 -6
  112. package/src/island-harness-script.ts +6 -2
  113. package/src/island-realtime.ts +15 -8
  114. package/src/island-shot.ts +5 -0
  115. package/src/island-store.ts +131 -0
  116. package/src/island-verdict.ts +1 -1
  117. package/src/job-registration.ts +42 -0
  118. package/src/jobs-driver.ts +2 -2
  119. package/src/load-findings.ts +51 -0
  120. package/src/mcp-db-target.ts +1 -1
  121. package/src/mcp-errors.ts +28 -27
  122. package/src/mcp-host.ts +27 -12
  123. package/src/mcp-ui-diff.ts +27 -0
  124. package/src/mcp-ui-inspect.ts +4 -4
  125. package/src/mcp-ui-interact.ts +21 -15
  126. package/src/mcp-ui.ts +23 -17
  127. package/src/measure-database.ts +73 -0
  128. package/src/measure-paths.ts +74 -0
  129. package/src/measure-scope.ts +74 -0
  130. package/src/messages.ts +1 -3
  131. package/src/metrics-endpoint.ts +1 -1
  132. package/src/otlp-export.ts +1 -1
  133. package/src/output.ts +6 -0
  134. package/src/permission-grants.ts +86 -0
  135. package/src/prerender-out.ts +25 -0
  136. package/src/prerender.ts +121 -102
  137. package/src/pwa-artifacts.ts +3 -3
  138. package/src/reexport-manifest.ts +2 -1
  139. package/src/registry.ts +80 -56
  140. package/src/role-realtime.ts +36 -0
  141. package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
  142. package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
  143. package/src/role-start-types.ts +112 -0
  144. package/src/{dev-roles.ts → role-start.ts} +42 -115
  145. package/src/{dev-sync.ts → role-sync.ts} +22 -8
  146. package/src/root-env.ts +67 -0
  147. package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
  148. package/src/{dev-services.ts → runtime-bindings.ts} +39 -16
  149. package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
  150. package/src/runtime-jobs.ts +87 -0
  151. package/src/{dev-live-feed.ts → runtime-live-feed.ts} +18 -5
  152. package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
  153. package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
  154. package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
  155. package/src/runtime-realtime.ts +55 -0
  156. package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
  157. package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
  158. package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
  159. package/src/scaffold-fixture.ts +28 -6
  160. package/src/scaffold-typecheck.ts +6 -3
  161. package/src/schema-drift.ts +7 -1
  162. package/src/script-csp.ts +5 -2
  163. package/src/secrets-rotation.ts +59 -0
  164. package/src/serve-boot.ts +192 -0
  165. package/src/serve-drain.ts +24 -0
  166. package/src/serve-entry.ts +6 -0
  167. package/src/serve-env.ts +116 -0
  168. package/src/serve-types.ts +55 -0
  169. package/src/serve.ts +44 -347
  170. package/src/shot-server.ts +2 -2
  171. package/src/shot-settle.ts +10 -1
  172. package/src/shot-theme.ts +3 -3
  173. package/src/shot-verdict.ts +16 -7
  174. package/src/signal-shred.ts +27 -0
  175. package/src/solid-loader.ts +26 -2
  176. package/src/static-report.ts +8 -1
  177. package/src/sync-url.ts +21 -1
  178. package/src/templates/action.ts +30 -16
  179. package/src/templates/entity.ts +12 -7
  180. package/src/templates/index.ts +1 -1
  181. package/src/templates/job.ts +10 -7
  182. package/src/templates/policy.ts +20 -2
  183. package/src/templates/resource-create.ts +127 -0
  184. package/src/templates/resource-form-island.ts +49 -12
  185. package/src/templates/resource.ts +10 -4
  186. package/src/templates/scaffold-app.ts +4 -1
  187. package/src/templates/scaffold-auth.ts +3 -1
  188. package/src/templates/scaffold-container-compose.ts +184 -0
  189. package/src/templates/scaffold-container.ts +24 -159
  190. package/src/templates/scaffold-dashboard-example.ts +2 -2
  191. package/src/templates/scaffold-db-package.ts +16 -3
  192. package/src/templates/scaffold-demo-org.ts +41 -0
  193. package/src/templates/scaffold-entries.ts +1 -1
  194. package/src/templates/scaffold-env.ts +6 -0
  195. package/src/templates/scaffold-helm-templates.ts +66 -7
  196. package/src/templates/scaffold-helm.ts +27 -5
  197. package/src/templates/scaffold-i18n.ts +15 -10
  198. package/src/templates/scaffold-repo.ts +13 -10
  199. package/src/templates/scaffold-roles.ts +38 -10
  200. package/src/templates/slice-foundation.ts +1 -1
  201. package/src/templates/wrap.ts +4 -1
  202. package/src/test-passes.ts +2 -1
  203. package/src/test-workers.ts +26 -0
  204. package/src/ts-scan.ts +3 -6
  205. package/src/tsconfig-references.ts +1 -2
  206. package/src/verify-checks.ts +41 -39
  207. package/src/verify-e2e.ts +10 -7
  208. package/src/verify-run.ts +11 -1
  209. package/src/verify-step.ts +3 -3
  210. package/src/verify-tests.ts +1 -26
  211. package/src/verify-typecheck.ts +28 -0
  212. package/src/web-binding.ts +2 -2
  213. package/src/workspace-graph.ts +10 -33
  214. package/src/cdp-browser.ts +0 -94
  215. package/src/cdp-connection.ts +0 -247
  216. package/src/cdp-e2e-page.ts +0 -180
  217. package/src/cdp-e2e-session.ts +0 -199
  218. package/src/cdp-errors.ts +0 -56
  219. package/src/cdp-launch.ts +0 -193
  220. package/src/cdp-offline-script.ts +0 -73
  221. package/src/cdp-pipe.ts +0 -77
  222. package/src/e2e-app.ts +0 -103
  223. package/src/e2e-browser-handle.ts +0 -55
  224. package/src/e2e-dom-fixture.ts +0 -117
  225. package/src/e2e-driver.ts +0 -117
  226. package/src/e2e-errors.ts +0 -117
  227. package/src/e2e-evaluate.ts +0 -156
  228. package/src/e2e-locator.ts +0 -86
  229. package/src/e2e-page.ts +0 -153
  230. package/src/e2e-preload.ts +0 -64
  231. package/src/e2e-probe.ts +0 -23
  232. package/src/e2e-selection.ts +0 -182
  233. package/src/e2e-spawn.ts +0 -169
  234. package/src/measurement-actor.ts +0 -26
  235. /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
  236. /package/src/{dev-render.ts → runtime-render.ts} +0 -0
@@ -15,12 +15,13 @@ import {
15
15
  import type { MetaIssue } from '@ultimat3/seo';
16
16
  import { validateMeta } from '@ultimat3/seo';
17
17
  import { checkAgentsMd } from './app-agents-md';
18
- import { checkAppBoundaries } from './app-boundaries';
19
18
  import { envExampleFindings } from './app-env';
20
19
  import { appManifest, readAppManifest } from './app-manifest';
21
- import { OPENAPI_FILE, openApiJson } from './app-openapi';
20
+ import { OPENAPI_FILE, openApiStaleness } from './app-openapi';
22
21
  import { policyFindings } from './app-permissions';
23
22
  import { APP_CONFIG_FILE } from './app-root';
23
+ import { asyncPageFindings } from './async-pages';
24
+ import { appBoundaryFindings } from './boundary-findings';
24
25
  import { checkBudgets, readBuildStats } from './budgets';
25
26
  import { checkDestructiveMigrations } from './db-destructive';
26
27
  import { checkUngeneratableMigrations } from './db-ungeneratable';
@@ -28,17 +29,21 @@ import { checkDocumentStyles, documentSurfaces } from './document-styles';
28
29
  import { checkErrorCodeResolution, checkErrorFixReport } from './error-contract';
29
30
  import { guardFindings } from './guards';
30
31
  import { catalogFindings } from './i18n-registration';
32
+ import { unregisteredJobFindings } from './job-registration';
31
33
  import { liveRouteFindings } from './live-routes';
34
+ import { withLoadFindings, withoutLoadFindings } from './load-findings';
32
35
  import { msg } from './messages';
33
36
  import type { Finding } from './output';
34
37
  import { findingFrom } from './output';
35
38
  import { checkMigrationDrift } from './schema-drift';
36
39
  import { scanSiteMeta } from './seo-meta';
40
+ import { quoteArg } from './shell-quote';
37
41
  import { readStaticReport } from './static-report';
38
42
  import { floorProblemFindings, readVerifyFloor } from './verify-floor';
39
43
  import type { VerifyStep } from './verify-step';
40
44
  import { fromExec, fromFindings, hostFindings } from './verify-step';
41
45
  import { TEST_STEPS } from './verify-tests';
46
+ import { typecheckArgs } from './verify-typecheck';
42
47
  import { checkFileSizes, checkPackageShape, hasWorkspacePackages } from './workspace-checks';
43
48
  import { checkWorkspaceDependencies } from './workspace-graph';
44
49
 
@@ -49,22 +54,21 @@ const ROADMAP_FILE = join('docs', 'idea', '14-roadmap.md');
49
54
  export const VERIFY_STEPS: readonly VerifyStep[] = [
50
55
  {
51
56
  name: 'typecheck',
52
- summary: 'tsc -b across every project the root references',
57
+ summary: 'tsc -b across every project the root references, tsc -p . where it references none',
53
58
  // `typecheckBin` (`x.verify.json`, beside `agentsMdMaxBytes`) swaps the binary and nothing
54
- // else: same `-b --pretty false` invocation, same output format to parse, same `X_TYPECHECK_
55
- // FAILED` finding either way. Absent means `tsc` — the only checker every app already has,
56
- // since `typescript` is a framework dependency and a drop-in replacement is the app's own
57
- // devDependency to add, never a default this step could assume.
59
+ // else: same invocation, same output format to parse, same `X_TYPECHECK_FAILED` finding either
60
+ // way. Absent means `tsc` — the only checker every app already has, since `typescript` is a
61
+ // framework dependency and a drop-in replacement is the app's own devDependency to add, never
62
+ // a default this step could assume. `-b` or `-p .` is `typecheckArgs`' call (#450).
58
63
  async run(ctx) {
59
64
  const floor = await readVerifyFloor(ctx.root);
60
- const bin = floor?.typecheckBin ?? 'tsc';
61
- const result = await ctx.runner(['bunx', bin, '-b', '--pretty', 'false'], {
62
- cwd: ctx.root,
63
- });
65
+ const argv = await typecheckArgs(ctx.root, floor?.typecheckBin ?? 'tsc');
66
+ const result = await ctx.runner(['bunx', ...argv], { cwd: ctx.root });
64
67
  return fromExec(result, {
65
68
  code: 'X_TYPECHECK_FAILED',
66
69
  cause: 'the project does not typecheck',
67
- fix: `bunx ${bin} -b --pretty false`,
70
+ // Joined, never spliced: `typecheckBin` is a value from the app's `x.verify.json`.
71
+ fix: ['bunx', ...argv.map(quoteArg)].join(' '),
68
72
  });
69
73
  },
70
74
  },
@@ -99,7 +103,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
99
103
  // extension model rejects.
100
104
  run: async (ctx) =>
101
105
  fromFindings([
102
- ...(await checkAppBoundaries(ctx.root)),
106
+ ...(await appBoundaryFindings(ctx.root)),
103
107
  ...(await guardFindings(ctx.root)),
104
108
  ...(await hostFindings(ctx, 'boundaries')),
105
109
  ]),
@@ -187,10 +191,11 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
187
191
  async run(ctx) {
188
192
  const committed = await readAppManifest(ctx.root);
189
193
  const { manifest, findings } = await appManifest(ctx.root);
190
- return fromFindings([
194
+ // A module that will not import is `manifest`'s to report (`load-findings.ts`).
195
+ return withoutLoadFindings(ctx.root, [
191
196
  ...findings,
192
197
  ...(committed === undefined ? [] : contractFindings(committed, manifest)),
193
- ...(await specFindings(ctx.root, manifest)),
198
+ ...(await openApiStaleness(ctx.root, manifest)),
194
199
  ]);
195
200
  },
196
201
  },
@@ -224,21 +229,27 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
224
229
  // reported under `X_ISLAND_PROPS_INVALID` — the build's own sentence, naming the prop and
225
230
  // its bytes — and not as an `X_BUDGET_UNMEASURED` whose fix is to go and read this file.
226
231
  const report = await readStaticReport(ctx.root);
227
- // The load's own findings, FIRST and never dropped. A module that would not import registers
228
- // no route, so its budget is missing from the manifest and every route it declared reads as
229
- // `X_BUDGET_UNMEASURED` — the symptom, pointing the reader at `x build` for a file that will
230
- // not compile. `contract-diff` reports these too when it applies; two red steps naming one
231
- // broken module is honest, and one of them silently green over it is the false green.
232
+ // The load's own findings are `manifest`'s, reported there once; this step's output says so.
233
+ // A module that would not import registers no route, so every route it declared still reads
234
+ // as `X_BUDGET_UNMEASURED` here — the symptom stays this step's, and the gate stays red.
232
235
  const { manifest, findings } = await appManifest(ctx.root);
233
- return fromFindings([
236
+ const outcome = await withoutLoadFindings(ctx.root, [
234
237
  ...findings,
235
238
  ...checkDocumentStyles(documentSurfaces()),
236
239
  // The third rider, and the same question this step already asks one level down: what
237
240
  // JavaScript does this route's document boot? A live read with no island is a route
238
241
  // whose answer is "none", which no suite can fail on — the page renders, at 200.
239
242
  ...(await liveRouteFindings(ctx.root)),
243
+ // The fourth: a data read in an async Page rather than in `load` (`async-pages.ts`).
244
+ ...asyncPageFindings(),
240
245
  ...checkBudgets(manifest, stats, report?.unmeasured),
241
246
  ]);
247
+ // What no build can weigh, said out loud rather than dropped (`measure-paths.ts`).
248
+ const notes = (report?.unmeasured ?? [])
249
+ .filter((one) => one.weighable === false)
250
+ .map((one) => `not weighed: ${one.reason}`);
251
+ const output = [outcome.output, ...notes].filter((line) => line !== undefined).join('\n');
252
+ return output === '' ? outcome : { ...outcome, output };
242
253
  },
243
254
  },
244
255
  {
@@ -293,7 +304,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
293
304
  // registers no routes — SKIPPED there, never passed, for the reason `i18n` gives: a step that
294
305
  // answers `ok` about nothing is the vacuous green these checks exist to refuse.
295
306
  applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
296
- run: async (ctx) => fromFindings(await policyFindings(ctx.root)),
307
+ run: async (ctx) => withoutLoadFindings(ctx.root, await policyFindings(ctx.root)),
297
308
  },
298
309
  {
299
310
  name: 'manifest',
@@ -315,9 +326,16 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
315
326
  // and reading it after the check would enforce the default on a repo that raised it.
316
327
  const floor = await readVerifyFloor(ctx.root);
317
328
  const agents = await checkAgentsMd(ctx.root, floor?.agentsMdMaxBytes);
329
+ // The app's load failures, once, here — every other step that loaded it points at this one.
330
+ const app = existsSync(join(ctx.root, APP_CONFIG_FILE));
331
+ const drift = await driftFindings(ctx.root);
318
332
  const findings = [
319
333
  ...manifestMissingFindings(ctx.root),
320
- ...(await driftFindings(ctx.root)),
334
+ ...(app ? await withLoadFindings(ctx.root, drift) : drift),
335
+ // An app only: the framework monorepo declares no `defineApi`, so it has no list to be in.
336
+ ...(existsSync(join(ctx.root, APP_CONFIG_FILE))
337
+ ? await unregisteredJobFindings(ctx.root)
338
+ : []),
321
339
  ...(await envExampleFindings(ctx.root)),
322
340
  ...floorProblemFindings(floor),
323
341
  ...agents.findings,
@@ -394,22 +412,6 @@ function contractFindings(before: Manifest, after: Manifest): readonly Finding[]
394
412
  }
395
413
  }
396
414
 
397
- /** The typed client is generated from `openapi.json`, so a stale spec ships a wrong client. */
398
- async function specFindings(root: string, manifest: Manifest): Promise<readonly Finding[]> {
399
- const path = join(root, OPENAPI_FILE);
400
- if (!existsSync(path)) return [];
401
- if ((await Bun.file(path).text()) === openApiJson(manifest)) return [];
402
- return [
403
- {
404
- code: 'X_MANIFEST_STALE',
405
- cause: `${OPENAPI_FILE} does not match the actions the code registers`,
406
- fix: 'x manifest',
407
- docs: ERROR_DOCS_URL,
408
- at: OPENAPI_FILE,
409
- },
410
- ];
411
- }
412
-
413
415
  /**
414
416
  * One `MetaIssue` as the gate reports it. `at` is the route FILE and never the URL: every seo error
415
417
  * already names the file in its cause, and `at` is what an agent opens.
package/src/verify-e2e.ts CHANGED
@@ -4,14 +4,17 @@
4
4
  // app: the suite runs as before and its browser-backed cases skip — or refuse under
5
5
  // `E2E_BROWSER_REQUIRED=1`.
6
6
 
7
- // why: Bun exposes no path API — the preload is addressed by an absolute path the child resolves.
8
- import { join } from 'node:path';
9
- import { findChrome } from './cdp-launch';
10
- import { E2E_ROOT_ENV } from './e2e-browser-handle';
7
+ import { E2E_ROOT_ENV, findChrome } from '@ultimat3/testing';
11
8
  import type { ExecResult } from './exec';
12
9
 
13
- /** The preload `bun test` is handed, beside the app's own from `bunfig.toml`. */
14
- export const E2E_PRELOAD = join(import.meta.dir, 'e2e-preload.ts');
10
+ /**
11
+ * The preload `bun test` is handed, beside the app's own from `bunfig.toml` — `@ultimat3/testing`'s,
12
+ * resolved from here to the absolute path the child process loads. A FUNCTION, resolved when a run
13
+ * needs it: at module scope it ran on every import of the registry, and a compiled `x` binary —
14
+ * whose `/$bunfs` holds no `node_modules` — died at boot resolving a file it never uses.
15
+ */
16
+ export const e2ePreload = (): string =>
17
+ Bun.resolveSync('@ultimat3/testing/e2e-preload', import.meta.dir);
15
18
 
16
19
  export interface E2eRun {
17
20
  readonly command: readonly string[];
@@ -32,7 +35,7 @@ export async function withE2eApp(
32
35
  if (chrome === undefined) return run({ command: input.command, env: input.env });
33
36
  const [bun = 'bun', test = 'test', ...rest] = input.command;
34
37
  return run({
35
- command: [bun, test, '--preload', E2E_PRELOAD, ...rest],
38
+ command: [bun, test, '--preload', e2ePreload(), ...rest],
36
39
  env: { ...input.env, [E2E_ROOT_ENV]: input.root },
37
40
  });
38
41
  }
package/src/verify-run.ts CHANGED
@@ -128,6 +128,8 @@ function verifySummary(input: {
128
128
  * files; the rest are in-process scans), so they run BESIDE the serial suites — `live` and `e2e`
129
129
  * are one worker each, Postgres- and browser-bound, and mostly waiting. `typecheck` stays first
130
130
  * and alone: `tsc -b` writes `.tsbuildinfo` and `dist/`, and `unit` saturates every core.
131
+ * `manifest` joined them 2026-09-23: it compares committed files against the code and writes
132
+ * nothing, and at the repo root its host checks are ~20 whole-tree reads that sat alone at the end.
131
133
  */
132
134
  export const BESIDE_SERIAL_SUITES: ReadonlySet<string> = new Set([
133
135
  'lint',
@@ -135,6 +137,7 @@ export const BESIDE_SERIAL_SUITES: ReadonlySet<string> = new Set([
135
137
  'filesize',
136
138
  'package-shape',
137
139
  'errors',
140
+ 'manifest',
138
141
  ]);
139
142
 
140
143
  /** The consecutive run of steps the static group overlaps, in the order `VERIFY_STEP_NAMES` holds. */
@@ -145,7 +148,14 @@ async function runStep(
145
148
  ctx: VerifyContext,
146
149
  floor: Awaited<ReturnType<typeof readVerifyFloor>>,
147
150
  ): Promise<StepResult> {
148
- const applies = step.applies === undefined ? true : await step.applies(ctx);
151
+ // Inside the step's own failure, like a throwing `run`: an `applies` that threw escaped every
152
+ // catch and aborted the whole gate, which is the one outcome `runVerify` exists to prevent.
153
+ let applies: boolean;
154
+ try {
155
+ applies = step.applies === undefined ? true : await step.applies(ctx);
156
+ } catch (error) {
157
+ return { name: step.name, ok: false, durationMs: 0, findings: [findingOf(error, step.name)] };
158
+ }
149
159
  if (!applies) {
150
160
  // A skip this repo already ruled out is not a skip. The step ran here before — the floor is
151
161
  // that claim, committed — so "nothing to check" now means the suite was deleted, and the
@@ -79,9 +79,9 @@ export interface VerifyContext {
79
79
  */
80
80
  readonly workers?: number;
81
81
  /**
82
- * ONE step, by name — an iteration loop, and the one thing here that IS a narrowing. Every
83
- * iteration of the whole gate costs ~18s (14s of it `tsc -b`), which is the cost of asking a
84
- * question about one step. It does not weaken axiom 5, and the two rules that keep it honest are
82
+ * ONE step, by name — an iteration loop, and the one thing here that IS a narrowing. The
83
+ * whole gate costs minutes (3m19s at the framework root on 12 cores, `As of 2026-09-23`), which
84
+ * is the cost of asking a question about one step. It does not weaken axiom 5, and the two rules that keep it honest are
85
85
  * mechanical rather than remembered: a run with this set prints `NOT A GATE RUN` in the summary
86
86
  * AND carries `notAGateRun` in `--json` (`verify-run.ts`), so no reader of either can mistake it
87
87
  * for the gate; and nothing writes `x.verify.json`, so the suite floor cannot be lowered by a
@@ -18,7 +18,7 @@ import { countsOf } from './test-counts';
18
18
  import { testEnvOverrides } from './test-dotenv';
19
19
  import type { TestFile } from './test-select';
20
20
  import { discoverTests } from './test-select';
21
- import { defaultWorkers } from './test-workers';
21
+ import { defaultWorkers, SERIAL_TYPES } from './test-workers';
22
22
  import { withE2eApp } from './verify-e2e';
23
23
  import type { StepOutcome, VerifyContext, VerifyStep } from './verify-step';
24
24
  import { fromExec, fromFindings } from './verify-step';
@@ -111,31 +111,6 @@ export const ownerOf = (path: string): TestType =>
111
111
  const disownedBy = (type: TypedTest): readonly string[] =>
112
112
  type === 'e2e' ? [`**/e2e/**/*.{${OUTRANKING_E2E.join(',')}}.test.*`] : [];
113
113
 
114
- /**
115
- * Which types run across worker processes, and why the other two cannot.
116
- *
117
- * Parallel is safe when the only thing a test file shares with another file is the database, and
118
- * the database is per worker by construction (`ULTIMATE_TEST_WORKER` → one clone of the migrated
119
- * template, `@ultimat3/testing`'s `acquireWorkerDatabase`). Every other process-global in this
120
- * framework — the permission set, the roles, the entity/action/query registries, the error-code
121
- * titles, the fixture bag — is handled by `--isolate` giving each FILE its own module registry.
122
- *
123
- * | Type | Why |
124
- * |---|---|
125
- * | `live` | **serial.** A logical replication slot and a publication are named at the Postgres
126
- * CLUSTER level, not inside a database, and this repo's own feed tests hard-code
127
- * `x_live_slot` / `x_live_pub` against `TEST_REPLICATION_URL` — the one server, never a per-worker
128
- * clone. Two workers would race `pg_create_logical_replication_slot` and the loser's failure would
129
- * read as a flake. A per-worker database does not isolate a cluster-wide object |
130
- * | `e2e` | **serial.** It runs against the *built output*: one `dist/`, one service-worker
131
- * registration, one browser profile. There is nothing per-worker to hand it, and the type is
132
- * seconds at most, so a split would buy a race and no time |
133
- *
134
- * The two are named here rather than tested for, because "can this type be sharded?" is a design
135
- * fact about the type, not something a run can discover about itself.
136
- */
137
- export const SERIAL_TYPES: readonly TestType[] = ['live', 'e2e'];
138
-
139
114
  const isSerial = (type: TestType): boolean => SERIAL_TYPES.includes(type);
140
115
 
141
116
  /**
@@ -0,0 +1,28 @@
1
+ // Which `tsc` invocation the `typecheck` step runs. `tsc -b` decides "up to date" by mtime, and Bun
2
+ // installs a package as HARDLINKS out of its cache, carrying the cache's old mtimes — so upgrading
3
+ // a dependency to a version already in the cache left every input older than the buildinfo, `-b`
4
+ // skipped the program and reported green while a cold CI went red (#450). `-p` with `incremental`
5
+ // re-reads every input and compares content hashes, so it cannot be fooled by a date.
6
+
7
+ import { checkRootReferences } from './tsconfig-references';
8
+
9
+ /**
10
+ * A package dir no reference can name. `checkRootReferences` answers `[]` when the root has no
11
+ * `references` array at all and one finding for this probe when it has one, which is the one
12
+ * question asked here — without a second JSONC parser beside the one that file owns.
13
+ */
14
+ const PROBE = '\u0000x-verify-typecheck-probe';
15
+
16
+ /** True when the root tsconfig declares `references`: a build graph that only `-b` can walk. */
17
+ export const usesProjectReferences = async (root: string): Promise<boolean> =>
18
+ (await checkRootReferences(root, [PROBE])).length > 0;
19
+
20
+ /**
21
+ * The argv after `bunx`. A root with references keeps `-b` — the framework's 32 projects are only
22
+ * reachable that way, and `tsc -b` rebuilds a project whose own sources moved. A root without them
23
+ * (every scaffolded app) is ONE program, and `-p .` is the content-hashed check of it.
24
+ */
25
+ export async function typecheckArgs(root: string, bin: string): Promise<readonly string[]> {
26
+ const mode = (await usesProjectReferences(root)) ? ['-b'] : ['-p', '.'];
27
+ return [bin, ...mode, '--pretty', 'false'];
28
+ }
@@ -1,6 +1,6 @@
1
1
  // How a process binds its sockets, and what it admits about itself. A LEAF: it imports nothing,
2
- // so every role can read it without pulling `dev-roles` — which is what made this its own file.
3
- // `dev-sync` needs the default and `dev-roles` already imports `dev-sync`, so reading it from
2
+ // so every role can read it without pulling `role-start` — which is what made this its own file.
3
+ // `role-sync` needs the default and `role-start` already imports `role-sync`, so reading it from
4
4
  // there would be a runtime import cycle in the framework's own boot path.
5
5
 
6
6
  export interface WebBinding {
@@ -5,9 +5,9 @@
5
5
 
6
6
  import { join } from 'node:path';
7
7
  import { ERROR_DOCS_URL } from '@ultimat3/core';
8
+ import { scanAllImports } from './import-scan';
8
9
  import type { Finding } from './output';
9
10
  import { eachSourceFile, isGenerated, isTest, isVendored } from './source-files';
10
- import { maskLiterals } from './ts-scan';
11
11
 
12
12
  export interface WorkspaceNode {
13
13
  /** package.json "name". */
@@ -144,43 +144,20 @@ export function packageOfSpecifier(specifier: string): string | undefined {
144
144
  }
145
145
 
146
146
  /**
147
- * Every form that names a module: `… from '…'`, a bare `import '…'`, `import('…')`, `require('…')`.
148
- * `from` is matched only where a quote follows it directly, so `from<Row>('posts', …)` — the query
149
- * builder, which reads exactly like an import — cannot be one.
147
+ * Every package a source file imports, deduplicated, in first-appearance order — type-only imports
148
+ * included, because a type edge still needs its manifest line. The transpiler is the parser, so a
149
+ * generator's template literal (even one nested in a `${…}`, #493) is a string and never an import,
150
+ * and `from<Row>('posts')` is a call. A file it cannot parse throws rather than reading as empty.
150
151
  */
151
- const IMPORT_FORM =
152
- /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|(?:^|[;{])\s*import\s+)['"]([^'"]*)['"]/g;
153
-
154
- /**
155
- * Every package a source file imports, deduplicated, in first-appearance order.
156
- *
157
- * Read from the MASKED text, because a generator's template literal holds whole programs: every
158
- * `templates/*.ts` here emits `import … from '@ultimat3/ui'` as a string, and a scan that read
159
- * those would bill the CLI for the imports of the app it writes. The line a masked hit falls on is
160
- * then re-read from the real source — the specifier itself is what masking blanks.
161
- */
162
- export function importedPackages(source: string): readonly string[] {
163
- const masked = maskLiterals(source).split('\n');
164
- const lines = source.split('\n');
152
+ export function importedPackages(source: string, path = 'source.ts'): readonly string[] {
165
153
  const packages = new Set<string>();
166
- for (const [index, maskedLine] of masked.entries()) {
167
- const line = lines[index];
168
- // `matchAll` clones the regex, so the shared `lastIndex` is never carried between lines.
169
- if (line === undefined || [...maskedLine.matchAll(IMPORT_FORM)].length === 0) continue;
170
- for (const match of line.matchAll(IMPORT_FORM)) {
171
- const name = packageOfSpecifier(match[1] ?? '');
172
- if (name !== undefined) packages.add(name);
173
- }
154
+ for (const specifier of scanAllImports({ path, source })) {
155
+ const name = packageOfSpecifier(specifier);
156
+ if (name !== undefined) packages.add(name);
174
157
  }
175
158
  return [...packages];
176
159
  }
177
160
 
178
- /**
179
- * Borrowed, not twinned: `X_APP_PACKAGE_INVALID` already means "this package.json supplies no
180
- * usable name", and a workspace manifest is that same file one directory down. `bun pm pkg set`
181
- * is not the fix here — it parses the file it edits, so it fails on exactly the input this
182
- * reports.
183
- */
184
161
  export const unreadableWorkspaceFinding = (path: string): Finding => ({
185
162
  code: 'X_APP_PACKAGE_INVALID',
186
163
  cause: `${path} is claimed by the root "workspaces" globs and supplies no readable "name"`,
@@ -228,7 +205,7 @@ export async function checkWorkspaceDependencies(root: string): Promise<readonly
228
205
  if (isTest(path) || isGenerated(path)) continue;
229
206
  const owner = owners.find((node) => path.startsWith(`${node.dir}/`));
230
207
  if (owner === undefined) continue;
231
- for (const name of importedPackages(await Bun.file(join(root, path)).text())) {
208
+ for (const name of importedPackages(await Bun.file(join(root, path)).text(), path)) {
232
209
  const target = byName.get(name);
233
210
  if (target === undefined || target.name === owner.name) continue;
234
211
  if (owner.dependencies.includes(target.name)) continue;
@@ -1,94 +0,0 @@
1
- // One responsibility: compose the halves — find a browser, launch it over its pipe, attach a page —
2
- // into the one object `installE2eDriver({ page })` takes, plus the way to shut it down.
3
- //
4
- // **Absent is a SKIP, never a failure, and that is a requirement rather than a state.** A CI box
5
- // with no Chrome must not turn the `e2e` step red for a reason unrelated to the change, which is
6
- // the rule `packages/cli/CLAUDE.md` already states about `x shot`, `x pr` and `x ci`.
7
- // `openE2eBrowserIfAvailable` is that door; `openE2eBrowser` refuses by name for a caller that has
8
- // already decided a browser is required.
9
-
10
- import { finiteCount } from '@ultimat3/core';
11
- import type { E2eTab } from './cdp-e2e-page';
12
- import type { E2eSession } from './cdp-e2e-session';
13
- import { cdpE2eSession } from './cdp-e2e-session';
14
- import { CdpBrowserMissingError } from './cdp-errors';
15
- import type { LaunchedBrowser } from './cdp-launch';
16
- import { CHROME_CANDIDATES, findChrome, launchChrome } from './cdp-launch';
17
-
18
- /** How long a launch, a connect or a single CDP call may take. One number, three deadlines. */
19
- export const DEFAULT_CDP_TIMEOUT_MS = 30_000;
20
-
21
- export interface E2eBrowser {
22
- /** The first tab — what `installE2eDriver({ page })` drives. */
23
- readonly page: E2eTab;
24
- /**
25
- * The browser itself: more tabs in the same profile, init scripts, the offline switch for every
26
- * page and worker, and the log of every socket and request. What a multi-tab acceptance suite
27
- * drives, on the same launch as `page` — one harness, never a second one beside the driver.
28
- */
29
- readonly session: E2eSession;
30
- /** Idempotent, and it closes both halves: the CDP socket, then the process and its profile. */
31
- close(): void;
32
- }
33
-
34
- export interface OpenE2eBrowserOptions {
35
- readonly env?: Readonly<Record<string, string | undefined>> | undefined;
36
- readonly timeoutMs?: number | undefined;
37
- }
38
-
39
- /**
40
- * Screened HERE, before a browser exists, and not where it lands. It becomes three deadlines — the
41
- * launch, the handshake and every CDP call — and `Number(process.env.E2E_TIMEOUT ?? '')` is `NaN`
42
- * for an unset variable and is not nullish, so `??` keeps it: a `setTimeout` given `NaN` fires at
43
- * 1ms in this Bun, which makes every call report `X_CDP_TIMEOUT` against a browser that was
44
- * answering. A misdiagnosis reported as a test failure is worse than the failure.
45
- */
46
- const budget = (options: OpenE2eBrowserOptions): number =>
47
- finiteCount('openE2eBrowser', 'timeoutMs', options.timeoutMs ?? DEFAULT_CDP_TIMEOUT_MS);
48
-
49
- const compose = (launched: LaunchedBrowser, session: E2eSession, page: E2eTab): E2eBrowser => ({
50
- page,
51
- session,
52
- // The connection first, then the process — `launched.close()` does both, in that order: closing
53
- // the process out from under an open connection makes every in-flight call report "the browser
54
- // closed the CDP connection", which is true and useless.
55
- close: () => launched.close(),
56
- });
57
-
58
- /**
59
- * Launch a browser and attach one page to it. Refuses with `X_CDP_BROWSER_MISSING` when there is
60
- * nothing to launch — the caller that wants a skip asks `openE2eBrowserIfAvailable` instead.
61
- */
62
- export async function openE2eBrowser(options: OpenE2eBrowserOptions = {}): Promise<E2eBrowser> {
63
- const timeoutMs = budget(options);
64
- const executable = await findChrome(options.env ?? process.env);
65
- if (executable === undefined) throw new CdpBrowserMissingError({ tried: CHROME_CANDIDATES });
66
- return openLaunched(executable, timeoutMs);
67
- }
68
-
69
- /** `undefined` when this machine has no browser. Every other failure still throws. */
70
- export async function openE2eBrowserIfAvailable(
71
- options: OpenE2eBrowserOptions = {},
72
- ): Promise<E2eBrowser | undefined> {
73
- const timeoutMs = budget(options);
74
- const executable = await findChrome(options.env ?? process.env);
75
- if (executable === undefined) return undefined;
76
- return openLaunched(executable, timeoutMs);
77
- }
78
-
79
- /**
80
- * The half both doors share. Each step undoes the ones before it on the way out: a Chrome that
81
- * launched and then refused the CDP handshake would otherwise be left running, holding its profile
82
- * directory, for the rest of the test process — one leaked browser per failing suite.
83
- */
84
- async function openLaunched(executable: string, timeoutMs: number): Promise<E2eBrowser> {
85
- const launched = await launchChrome({ executable, timeoutMs });
86
- const { connection } = launched;
87
- try {
88
- const session = await cdpE2eSession({ connection, loadTimeoutMs: timeoutMs });
89
- return compose(launched, session, await session.newTab());
90
- } catch (error) {
91
- launched.close();
92
- throw error;
93
- }
94
- }