@ultimat3/cli 20.2.1 → 22.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 (237) hide show
  1. package/CLAUDE.md +84 -1582
  2. package/README.md +40 -2
  3. package/package.json +31 -31
  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-env.ts +2 -2
  8. package/src/app-load.ts +37 -5
  9. package/src/app-openapi.ts +27 -0
  10. package/src/app-permissions.ts +0 -0
  11. package/src/app-root.ts +1 -1
  12. package/src/async-pages.ts +32 -0
  13. package/src/bin.ts +7 -1
  14. package/src/boundary-findings.ts +36 -0
  15. package/src/browser-launcher-fake-html.ts +80 -0
  16. package/src/browser-launcher-fake.ts +165 -0
  17. package/src/browser-launcher-port.ts +159 -0
  18. package/src/browser-launcher.ts +26 -133
  19. package/src/budgets.ts +56 -13
  20. package/src/build-errors.ts +54 -0
  21. package/src/cdp-shot-a11y.ts +77 -0
  22. package/src/cdp-shot-clock.ts +14 -0
  23. package/src/cdp-shot-driver.ts +150 -0
  24. package/src/cdp-shot-element.ts +147 -0
  25. package/src/cdp-shot-errors.ts +62 -0
  26. package/src/cdp-shot-keys.ts +152 -0
  27. package/src/cdp-shot-page.ts +230 -0
  28. package/src/cdp-shot-watch.ts +241 -0
  29. package/src/cmd-affected-spec.ts +24 -0
  30. package/src/cmd-affected.ts +3 -19
  31. package/src/cmd-build-spec.ts +16 -0
  32. package/src/cmd-build.ts +46 -20
  33. package/src/cmd-ci-spec.ts +24 -0
  34. package/src/cmd-ci.ts +17 -21
  35. package/src/cmd-db-branch.ts +2 -2
  36. package/src/cmd-db-spec.ts +99 -0
  37. package/src/cmd-db.ts +15 -94
  38. package/src/cmd-deploy-helm.ts +136 -0
  39. package/src/cmd-deploy-spec.ts +40 -0
  40. package/src/cmd-deploy.ts +117 -42
  41. package/src/cmd-dev-spec.ts +25 -0
  42. package/src/cmd-dev.ts +78 -122
  43. package/src/cmd-docs-spec.ts +16 -0
  44. package/src/cmd-docs.ts +2 -11
  45. package/src/cmd-doctor-spec.ts +21 -0
  46. package/src/cmd-doctor.ts +4 -18
  47. package/src/cmd-env-spec.ts +18 -0
  48. package/src/cmd-env.ts +4 -13
  49. package/src/cmd-errors-spec.ts +23 -0
  50. package/src/cmd-errors.ts +3 -17
  51. package/src/cmd-fix-spec.ts +18 -0
  52. package/src/cmd-fix.ts +4 -13
  53. package/src/cmd-generate-spec.ts +31 -0
  54. package/src/cmd-generate.ts +46 -42
  55. package/src/cmd-i18n-spec.ts +16 -0
  56. package/src/cmd-i18n.ts +7 -14
  57. package/src/cmd-jobs-spec.ts +56 -0
  58. package/src/cmd-jobs.ts +3 -50
  59. package/src/cmd-manifest-spec.ts +15 -0
  60. package/src/cmd-manifest.ts +19 -20
  61. package/src/cmd-mcp-spec.ts +21 -0
  62. package/src/cmd-mcp.ts +2 -15
  63. package/src/cmd-new-spec.ts +33 -0
  64. package/src/cmd-new.ts +38 -33
  65. package/src/cmd-policy-spec.ts +13 -0
  66. package/src/cmd-policy.ts +2 -9
  67. package/src/cmd-pr-spec.ts +39 -0
  68. package/src/cmd-pr.ts +5 -65
  69. package/src/cmd-registries-spec.ts +32 -0
  70. package/src/cmd-registries.ts +4 -25
  71. package/src/cmd-routes-spec.ts +12 -0
  72. package/src/cmd-routes.ts +2 -7
  73. package/src/cmd-secrets-spec.ts +19 -0
  74. package/src/cmd-secrets.ts +22 -34
  75. package/src/cmd-shot-island.ts +12 -13
  76. package/src/cmd-shot-spec.ts +53 -0
  77. package/src/cmd-shot.ts +11 -60
  78. package/src/cmd-tasks-spec.ts +21 -0
  79. package/src/cmd-tasks.ts +2 -16
  80. package/src/cmd-test-spec.ts +54 -0
  81. package/src/cmd-test.ts +4 -49
  82. package/src/cmd-verify-spec.ts +28 -0
  83. package/src/cmd-verify.ts +3 -23
  84. package/src/db-seed.ts +4 -2
  85. package/src/dev-dashboard.ts +4 -4
  86. package/src/dev-lock.ts +18 -2
  87. package/src/dev-port.ts +21 -0
  88. package/src/dev-route-table.ts +119 -0
  89. package/src/dispatch.ts +12 -5
  90. package/src/document-styles.ts +1 -1
  91. package/src/drift.ts +4 -18
  92. package/src/error-catalog.ts +16 -9
  93. package/src/error-codes.ts +33 -28
  94. package/src/error-pages.ts +19 -7
  95. package/src/error-unthrown.ts +130 -0
  96. package/src/errors.ts +22 -29
  97. package/src/favicon.ts +2 -2
  98. package/src/fix-imports.ts +1 -1
  99. package/src/fix-scan.ts +2 -9
  100. package/src/flag-reads.ts +1 -2
  101. package/src/foreign-text.ts +36 -0
  102. package/src/framework-schema.ts +5 -6
  103. package/src/generate-feature.ts +42 -0
  104. package/src/generate-files.ts +7 -3
  105. package/src/generate-grants.ts +83 -0
  106. package/src/generate-kinds.ts +56 -4
  107. package/src/i18n-index.ts +59 -10
  108. package/src/icon-assets.ts +1 -1
  109. package/src/image-prepare.ts +14 -0
  110. package/src/import-scan.ts +63 -0
  111. package/src/index.ts +67 -334
  112. package/src/invocation-flags.ts +26 -0
  113. package/src/island-bundle.ts +38 -11
  114. package/src/island-capture.ts +6 -6
  115. package/src/island-harness-script.ts +6 -2
  116. package/src/island-realtime.ts +98 -0
  117. package/src/island-shot.ts +5 -0
  118. package/src/island-store.ts +131 -0
  119. package/src/island-verdict.ts +2 -2
  120. package/src/job-registration.ts +42 -0
  121. package/src/jobs-driver.ts +2 -2
  122. package/src/live-routes.ts +82 -42
  123. package/src/load-findings.ts +51 -0
  124. package/src/mcp-db-target.ts +1 -1
  125. package/src/mcp-errors.ts +30 -26
  126. package/src/mcp-host.ts +27 -12
  127. package/src/mcp-ui-diff.ts +27 -0
  128. package/src/mcp-ui-inspect.ts +4 -4
  129. package/src/mcp-ui-interact.ts +21 -15
  130. package/src/mcp-ui.ts +19 -15
  131. package/src/measure-database.ts +73 -0
  132. package/src/measure-paths.ts +74 -0
  133. package/src/measure-scope.ts +74 -0
  134. package/src/messages.ts +1 -3
  135. package/src/metrics-endpoint.ts +1 -1
  136. package/src/otlp-export.ts +1 -1
  137. package/src/output.ts +6 -0
  138. package/src/page-sync.ts +54 -0
  139. package/src/permission-grants.ts +86 -0
  140. package/src/prerender-out.ts +25 -0
  141. package/src/prerender.ts +121 -102
  142. package/src/pwa-artifacts.ts +3 -3
  143. package/src/realtime-browser-probe-fixture.ts +2 -2
  144. package/src/reexport-manifest.ts +2 -1
  145. package/src/registry.ts +80 -56
  146. package/src/role-realtime.ts +36 -0
  147. package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
  148. package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
  149. package/src/role-start-types.ts +112 -0
  150. package/src/{dev-roles.ts → role-start.ts} +42 -115
  151. package/src/{dev-sync.ts → role-sync.ts} +23 -11
  152. package/src/root-env.ts +67 -0
  153. package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
  154. package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
  155. package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
  156. package/src/runtime-jobs.ts +87 -0
  157. package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
  158. package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
  159. package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
  160. package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
  161. package/src/runtime-realtime.ts +55 -0
  162. package/src/{dev-render.ts → runtime-render.ts} +119 -20
  163. package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
  164. package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
  165. package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
  166. package/src/scaffold-fixture.ts +28 -6
  167. package/src/scaffold-typecheck.ts +6 -3
  168. package/src/schema-drift.ts +7 -1
  169. package/src/script-csp.ts +5 -2
  170. package/src/secrets-rotation.ts +59 -0
  171. package/src/serve-boot.ts +192 -0
  172. package/src/serve-drain.ts +24 -0
  173. package/src/serve-entry.ts +6 -0
  174. package/src/serve-env.ts +116 -0
  175. package/src/serve-types.ts +55 -0
  176. package/src/serve.ts +44 -338
  177. package/src/shot-server.ts +2 -2
  178. package/src/shot-settle.ts +10 -1
  179. package/src/shot-theme.ts +3 -3
  180. package/src/shot-verdict.ts +16 -7
  181. package/src/signal-shred.ts +27 -0
  182. package/src/solid-loader.ts +26 -2
  183. package/src/static-report.ts +8 -1
  184. package/src/sw-artifacts.ts +13 -3
  185. package/src/sync-url.ts +31 -0
  186. package/src/templates/action.ts +30 -16
  187. package/src/templates/entity.ts +12 -7
  188. package/src/templates/index.ts +1 -1
  189. package/src/templates/job.ts +10 -7
  190. package/src/templates/policy.ts +20 -2
  191. package/src/templates/resource-create.ts +127 -0
  192. package/src/templates/resource-form-island.ts +76 -30
  193. package/src/templates/resource.ts +10 -4
  194. package/src/templates/route.ts +3 -0
  195. package/src/templates/scaffold-app.ts +4 -1
  196. package/src/templates/scaffold-auth.ts +3 -1
  197. package/src/templates/scaffold-container-compose.ts +184 -0
  198. package/src/templates/scaffold-container.ts +25 -145
  199. package/src/templates/scaffold-dashboard-example.ts +2 -2
  200. package/src/templates/scaffold-db-package.ts +16 -3
  201. package/src/templates/scaffold-demo-org.ts +41 -0
  202. package/src/templates/scaffold-entries.ts +1 -1
  203. package/src/templates/scaffold-env.ts +6 -0
  204. package/src/templates/scaffold-helm-templates.ts +66 -7
  205. package/src/templates/scaffold-helm.ts +27 -5
  206. package/src/templates/scaffold-i18n.ts +15 -10
  207. package/src/templates/scaffold-repo.ts +13 -10
  208. package/src/templates/scaffold-roles.ts +38 -10
  209. package/src/templates/slice-foundation.ts +1 -1
  210. package/src/templates/wrap.ts +4 -1
  211. package/src/test-passes.ts +2 -1
  212. package/src/test-workers.ts +26 -0
  213. package/src/ts-scan.ts +3 -6
  214. package/src/tsconfig-references.ts +1 -2
  215. package/src/verify-checks.ts +41 -39
  216. package/src/verify-e2e.ts +41 -0
  217. package/src/verify-run.ts +115 -50
  218. package/src/verify-step.ts +3 -3
  219. package/src/verify-tests.ts +22 -30
  220. package/src/verify-typecheck.ts +28 -0
  221. package/src/web-binding.ts +2 -2
  222. package/src/worker-bundle.ts +192 -0
  223. package/src/workspace-graph.ts +10 -33
  224. package/src/cdp-browser.ts +0 -100
  225. package/src/cdp-connection.ts +0 -211
  226. package/src/cdp-e2e-page.ts +0 -209
  227. package/src/cdp-errors.ts +0 -56
  228. package/src/cdp-launch.ts +0 -139
  229. package/src/e2e-dom-fixture.ts +0 -117
  230. package/src/e2e-driver.ts +0 -97
  231. package/src/e2e-errors.ts +0 -103
  232. package/src/e2e-evaluate.ts +0 -156
  233. package/src/e2e-locator.ts +0 -86
  234. package/src/e2e-page.ts +0 -150
  235. package/src/e2e-selection.ts +0 -182
  236. package/src/measurement-actor.ts +0 -26
  237. /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
@@ -64,9 +64,9 @@ const rootPackage = (app: NameSet, version: string): string => `{
64
64
  "dev": "x dev",
65
65
  "check": "bin/check",
66
66
  "verify": "x verify",
67
- "typecheck": "tsc -b --pretty",
67
+ "typecheck": "tsc -p . --pretty",
68
68
  "lint": "biome check .",
69
- "test": "bun test",
69
+ "test": "bun test --isolate",
70
70
  "db:migrate": "x db migrate",
71
71
  "db:seed": "x db seed"
72
72
  },
@@ -89,6 +89,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
89
89
  "@ultimat3/i18n": "^${version}",
90
90
  "@ultimat3/jobs": "^${version}",
91
91
  "@ultimat3/mcp": "^${version}",
92
+ "@ultimat3/money": "^${version}",
92
93
  "@ultimat3/policy": "^${version}",
93
94
  "@ultimat3/pwa": "^${version}",
94
95
  "@ultimat3/query": "^${version}",
@@ -106,14 +107,16 @@ const rootPackage = (app: NameSet, version: string): string => `{
106
107
 
107
108
  /**
108
109
  * `"incremental": true` is ONE line and it is the difference between a 4.9s typecheck and a 92s
109
- * one. `x verify`'s first step is `tsc -b`, and `-b` decides "up to date?" by comparing emitted
110
- * OUTPUTS against inputs — with `noEmit` and no `composite`/`references`, the output it looks for
111
- * is an `app.config.js` that will never exist (`Project 'tsconfig.json' is out of date because
112
- * output file 'app.config.js' does not exist`), so every run rebuilt the whole program from
113
- * scratch, forever. Measured on a 166-file scaffold with no source change between runs: 92s wall
114
- * / 43s user CPU without it, 4.9s / 8.8s warm with it, and the whole gate at 12s rather than
115
- * 24-71s. This was the only tree in the framework without incremental typechecking — the repo
116
- * root has 32 `references` and `examples/dummy/tsconfig.json` sets `composite`.
110
+ * one. Measured on a 166-file scaffold with no source change between runs: 92s wall / 43s user CPU
111
+ * without it, 4.9s / 8.8s warm with it.
112
+ *
113
+ * The typecheck is `tsc -p .`, never `-b`, `As of 2026-09-23` (#450) — here, in the scaffold's
114
+ * `typecheck` script, and in `x verify`'s step (`verify-typecheck.ts`, for any root without
115
+ * `references`). `-b` decides "up to date?" by MTIME, and Bun installs a dependency as hardlinks
116
+ * carrying its cache's old mtimes: upgrading to a version already in the cache left every input
117
+ * older than the buildinfo, so `-b` skipped the program and reported green while a cold CI was
118
+ * red. `-p` with `incremental` compares content hashes. `-b` also wanted an emitted
119
+ * `app.config.js` that `noEmit` never writes, which is why this line was first needed.
117
120
  *
118
121
  * The note lives HERE and not in the emitted file, for the reason `biome.json` below gives: an
119
122
  * app author has no use for eight lines of framework archaeology in their own tsconfig, and
@@ -4,9 +4,23 @@
4
4
  // many answers as the app has folders, and the framework's two tracked apps already disagree.
5
5
 
6
6
  import type { GeneratedFile } from './naming';
7
+ import { wrapList } from './wrap';
7
8
 
8
- const rolesSource =
9
- (): string => `// Who holds which permission, for the whole app. Roles are sugar: every one expands to a flat
9
+ /**
10
+ * A role's grants as Biome prints them. The example slice's \`post:read\`/\`post:write\` ride along
11
+ * with it: \`x g resource post\` would have granted them, and the scaffold writes that result.
12
+ */
13
+ const grants = (base: string, example: string | undefined): string =>
14
+ wrapList(
15
+ ' ',
16
+ 'grants: [',
17
+ [base, ...(example === undefined ? [] : [example])].map((grant) => `'${grant}'`),
18
+ '],',
19
+ );
20
+
21
+ const rolesSource = (
22
+ example: boolean,
23
+ ): string => `// Who holds which permission, for the whole app. Roles are sugar: every one expands to a flat
10
24
  // permission set before any policy runs, so a rule never reasons about the hierarchy.
11
25
  //
12
26
  // ONE file, and it lives in shared/ — the leaf both site/ and app/ already import, and the one the
@@ -16,8 +30,9 @@ const rolesSource =
16
30
  // role HERE; calling defineRoles() again from a feature folder works and is the drift this file
17
31
  // exists to prevent.
18
32
  //
19
- // \`x g policy <feature>\` declares \`<feature>:read\` and \`<feature>:write\`. Granting them is this
20
- // file's job — a permission no role holds is one no actor can ever exercise.
33
+ // \`x g policy <feature>\` declares \`<feature>:read\` and \`<feature>:write\` and grants them below —
34
+ // read to member, write to admin. A permission no role holds is one no actor can ever exercise,
35
+ // and \`x verify\`'s policy step refuses it (X_PERMISSION_UNGRANTED).
21
36
 
22
37
  import { definePermissions, defineRoles } from '@ultimat3/policy';
23
38
 
@@ -32,11 +47,11 @@ export const appPermissions = definePermissions(['admin:read', 'dashboard:read']
32
47
  export const roles = defineRoles({
33
48
  member: {
34
49
  description: 'Signed in. Reads the app surface.',
35
- grants: ['dashboard:read'],
50
+ ${grants('dashboard:read', example ? 'post:read' : undefined)}
36
51
  },
37
52
  admin: {
38
53
  description: 'Runs the app: the /admin surface, plus everything a member may do.',
39
- grants: ['admin:read'],
54
+ ${grants('admin:read', example ? 'post:write' : undefined)}
40
55
  inherits: ['member'],
41
56
  },
42
57
  });
@@ -49,12 +64,25 @@ import { expandRoles, isKnownPermission, rolesGranting } from '@ultimat3/policy'
49
64
  import { expect, unitTest } from '@ultimat3/testing';
50
65
  import { appPermissions, roles } from './roles';
51
66
 
67
+ // Every feature's policy.ts, imported the way the boot scan imports it: \`x g policy\` grants the
68
+ // permissions a policy.ts DECLARES, so the registry below is only whole once they have run.
69
+ const features = new Bun.Glob('{app,site}/*/policy.ts');
70
+ for await (const file of features.scan({ cwd: \`\${import.meta.dir}/..\` })) {
71
+ await import(\`\${import.meta.dir}/../\${file}\`);
72
+ }
73
+
52
74
  // The map is passed explicitly rather than read off the module-global one: a test that depended on
53
75
  // which module imported first would pass alone and fail inside a suite.
54
76
 
77
+ // Subsets, never exact lists: every \`x g policy\` adds a grant here, and a pinned list would make
78
+ // the generator's correct edit a red test.
55
79
  unitTest('admin inherits every member grant and adds its own', () => {
56
- expect(expandRoles(['member'], roles)).toEqual(['dashboard:read']);
57
- expect(expandRoles(['admin'], roles)).toEqual(['admin:read', 'dashboard:read']);
80
+ const member = expandRoles(['member'], roles);
81
+ const admin = expandRoles(['admin'], roles);
82
+ expect(member).toContain('dashboard:read');
83
+ expect(member).not.toContain('admin:read');
84
+ expect(admin).toContain('admin:read');
85
+ for (const permission of member) expect(admin).toContain(permission);
58
86
  });
59
87
 
60
88
  unitTest('a role nobody declared grants nothing', () => {
@@ -88,9 +116,9 @@ unitTest('the routes this app ships require permissions this app declares', () =
88
116
  `;
89
117
 
90
118
  /** `apps/web/shared/roles.ts` and its test. Written by `x new`, with or without the example slice. */
91
- export function rolesFiles(): readonly GeneratedFile[] {
119
+ export function rolesFiles(example = false): readonly GeneratedFile[] {
92
120
  return [
93
- { path: 'apps/web/shared/roles.ts', contents: rolesSource() },
121
+ { path: 'apps/web/shared/roles.ts', contents: rolesSource(example) },
94
122
  { path: 'apps/web/shared/roles.test.ts', contents: rolesTest() },
95
123
  ];
96
124
  }
@@ -3,7 +3,7 @@
3
3
  // `policyFiles`; the five generators that write *into* a slice imported the same files and wrote
4
4
  // none of them, so each emitted TS2307 in any slice a resource had not been run in first.
5
5
 
6
- import { stripComments } from '../ts-scan';
6
+ import { stripComments } from '@ultimat3/core';
7
7
  import type { FeatureTarget } from './entity';
8
8
  import { entityFiles } from './entity';
9
9
  import type { GeneratedFile, NameSet } from './naming';
@@ -80,7 +80,10 @@ const compareSpecifiers = (left: string, right: string): number => {
80
80
  * than spelled in each template, because the correct spelling is not knowable at authoring time.
81
81
  */
82
82
  export const sortSpecifiers = (names: readonly string[]): readonly string[] =>
83
- [...names].sort(compareSpecifiers);
83
+ [...names].sort((left, right) => compareSpecifiers(bare(left), bare(right)));
84
+
85
+ /** An inline \`type X\` sorts as \`X\`: Biome orders by the imported name, never by the modifier. */
86
+ const bare = (name: string): string => (name.startsWith('type ') ? name.slice(5) : name);
84
87
 
85
88
  /**
86
89
  * A named import, sorted and wrapped. Its own function because the braces are spaced on one line
@@ -3,8 +3,9 @@
3
3
  // this file decides how many, and `test-shards.ts` spends them.
4
4
 
5
5
  import type { TestFile } from './test-select';
6
+ import { SERIAL_TYPES } from './test-workers';
6
7
  import type { TestType } from './verify-tests';
7
- import { ownerOf, SERIAL_TYPES } from './verify-tests';
8
+ import { ownerOf } from './verify-tests';
8
9
 
9
10
  /** One `bun test` invocation: which files, how wide, and the type its reproduce line names. */
10
11
  export interface TestPass {
@@ -5,6 +5,7 @@
5
5
 
6
6
  // Bun ships no CPU-count primitive; `cpus()` is the fallback when navigator cannot answer.
7
7
  import { cpus } from 'node:os';
8
+ import type { TestType } from '@ultimat3/testing';
8
9
 
9
10
  /** navigator first: it is the runtime's own answer, and it respects a container's CPU limit. */
10
11
  export function availableCpus(): number {
@@ -48,3 +49,28 @@ export const WORKER_FLOOR = 2;
48
49
 
49
50
  export const defaultWorkers = (available: number = availableCpus()): number =>
50
51
  Math.max(WORKER_FLOOR, Math.min(WORKER_CEILING, Math.round(available * WORKER_OVERSUBSCRIBE)));
52
+
53
+ /**
54
+ * Which types run across worker processes, and why the other two cannot.
55
+ *
56
+ * Parallel is safe when the only thing a test file shares with another file is the database, and
57
+ * the database is per worker by construction (`ULTIMATE_TEST_WORKER` → one clone of the migrated
58
+ * template, `@ultimat3/testing`'s `acquireWorkerDatabase`). Every other process-global in this
59
+ * framework — the permission set, the roles, the entity/action/query registries, the error-code
60
+ * titles, the fixture bag — is handled by `--isolate` giving each FILE its own module registry.
61
+ *
62
+ * | Type | Why |
63
+ * |---|---|
64
+ * | `live` | **serial.** A logical replication slot and a publication are named at the Postgres
65
+ * CLUSTER level, not inside a database, and this repo's own feed tests hard-code
66
+ * `x_live_slot` / `x_live_pub` against `TEST_REPLICATION_URL` — the one server, never a per-worker
67
+ * clone. Two workers would race `pg_create_logical_replication_slot` and the loser's failure would
68
+ * read as a flake. A per-worker database does not isolate a cluster-wide object |
69
+ * | `e2e` | **serial.** It runs against the *built output*: one `dist/`, one service-worker
70
+ * registration, one browser profile. There is nothing per-worker to hand it, and the type is
71
+ * seconds at most, so a split would buy a race and no time |
72
+ *
73
+ * The two are named here rather than tested for, because "can this type be sharded?" is a design
74
+ * fact about the type, not something a run can discover about itself.
75
+ */
76
+ export const SERIAL_TYPES: readonly TestType[] = ['live', 'e2e'];
package/src/ts-scan.ts CHANGED
@@ -30,14 +30,11 @@ export interface CodeScan {
30
30
  readonly unresolved: readonly UnresolvedCodeSite[];
31
31
  }
32
32
 
33
- // The masking itself — `QUOTES`, `endOfLiteral`, `stripComments`, `maskLiterals` — lives in
34
- // `@ultimat3/core`'s `source-mask.ts` since 2026-09-05, because `@ultimat3/i18n`'s key extractor
35
- // (tier 1) needed it and could not reach a tier-5 package. Re-exported here so every scanner in
36
- // this package, and this package's public API, keep the names they had.
33
+ // The masking itself — `QUOTES`, `endOfLiteral`, `stripComments`, `maskLiterals` — is
34
+ // `@ultimat3/core`'s (`source-mask.ts`), imported from there by every scanner. It was re-exported
35
+ // here and from this package's barrel: two import paths for one function (22.0.0 removed both).
37
36
  import { endOfLiteral, maskLiterals, QUOTES, stripComments } from '@ultimat3/core';
38
37
 
39
- export { endOfLiteral, maskLiterals, QUOTES, stripComments };
40
-
41
38
  // `ReadonlySet`, so a consumer cannot mutate what every scan in this package reads.
42
39
  export const OPENERS: ReadonlySet<string> = new Set(['(', '[', '{']);
43
40
  export const CLOSERS: ReadonlySet<string> = new Set([')', ']', '}']);
@@ -7,9 +7,8 @@
7
7
  // root that has no `tsconfig.json` at all, so an `existsSync` ahead of it was a second question
8
8
  // with one answer.
9
9
  import { join } from 'node:path';
10
- import { ERROR_DOCS_URL } from '@ultimat3/core';
10
+ import { ERROR_DOCS_URL, maskLiterals, stripComments } from '@ultimat3/core';
11
11
  import type { Finding } from './output';
12
- import { maskLiterals, stripComments } from './ts-scan';
13
12
 
14
13
  const ROOT_TSCONFIG = 'tsconfig.json';
15
14
 
@@ -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.
@@ -0,0 +1,41 @@
1
+ // What the gate's `e2e` step does around the suite in an APP: when this machine has a browser, it
2
+ // runs the suite with the e2e preload and names the app root, and the PRELOAD spawns the app — so
3
+ // the app lives in the test process, where `deploy.newBuild()` can restart it. No browser, or not an
4
+ // app: the suite runs as before and its browser-backed cases skip — or refuse under
5
+ // `E2E_BROWSER_REQUIRED=1`.
6
+
7
+ import { E2E_ROOT_ENV, findChrome } from '@ultimat3/testing';
8
+ import type { ExecResult } from './exec';
9
+
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);
18
+
19
+ export interface E2eRun {
20
+ readonly command: readonly string[];
21
+ readonly env: Readonly<Record<string, string | undefined>>;
22
+ }
23
+
24
+ /** Wrap one e2e `bun test` invocation: `run` receives the command and extra environment to use. */
25
+ export async function withE2eApp(
26
+ input: {
27
+ readonly root: string;
28
+ readonly isApp: boolean;
29
+ readonly command: readonly string[];
30
+ readonly env: Readonly<Record<string, string | undefined>>;
31
+ },
32
+ run: (e2e: E2eRun) => Promise<ExecResult>,
33
+ ): Promise<ExecResult> {
34
+ const chrome = input.isApp ? await findChrome(Bun.env) : undefined;
35
+ if (chrome === undefined) return run({ command: input.command, env: input.env });
36
+ const [bun = 'bun', test = 'test', ...rest] = input.command;
37
+ return run({
38
+ command: [bun, test, '--preload', e2ePreload(), ...rest],
39
+ env: { ...input.env, [E2E_ROOT_ENV]: input.root },
40
+ });
41
+ }