@ultimat3/cli 22.15.0 → 24.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 (267) hide show
  1. package/CLAUDE.md +36 -13
  2. package/README.md +31 -8
  3. package/package.json +29 -28
  4. package/src/admin-catalog-keys.ts +16 -0
  5. package/src/admin-registration.ts +192 -0
  6. package/src/affected.ts +27 -35
  7. package/src/api-registration.ts +125 -11
  8. package/src/app-artifacts.ts +1 -1
  9. package/src/app-auth.ts +4 -27
  10. package/src/app-boundaries.ts +6 -20
  11. package/src/app-config-load.ts +51 -0
  12. package/src/app-env.ts +5 -11
  13. package/src/app-load.ts +55 -11
  14. package/src/app-manifest.ts +16 -8
  15. package/src/app-mcp.ts +10 -23
  16. package/src/app-permissions-borrowed.ts +129 -0
  17. package/src/app-permissions-site.ts +24 -0
  18. package/src/app-permissions.ts +0 -0
  19. package/src/app-reload-graph.ts +7 -30
  20. package/src/app-transport.ts +219 -0
  21. package/src/boundary-findings.ts +16 -4
  22. package/src/browser-transport.ts +160 -0
  23. package/src/budgets.ts +27 -34
  24. package/src/cdp-shot-driver.ts +3 -1
  25. package/src/cmd-auth-spec.ts +16 -0
  26. package/src/cmd-auth.ts +65 -0
  27. package/src/cmd-build-spec.ts +10 -3
  28. package/src/cmd-build.ts +107 -11
  29. package/src/cmd-db-backfill.ts +25 -12
  30. package/src/cmd-db.ts +66 -16
  31. package/src/cmd-deploy-helm.ts +7 -6
  32. package/src/cmd-deploy.ts +4 -4
  33. package/src/cmd-dev.ts +9 -8
  34. package/src/cmd-doctor-spec.ts +2 -9
  35. package/src/cmd-doctor.ts +75 -24
  36. package/src/cmd-env.ts +26 -2
  37. package/src/cmd-generate.ts +195 -47
  38. package/src/cmd-help.ts +19 -6
  39. package/src/cmd-i18n.ts +10 -9
  40. package/src/cmd-jobs-spec.ts +19 -3
  41. package/src/cmd-jobs.ts +136 -7
  42. package/src/cmd-manifest-spec.ts +1 -1
  43. package/src/cmd-manifest.ts +6 -25
  44. package/src/cmd-mcp.ts +46 -15
  45. package/src/cmd-new.ts +15 -1
  46. package/src/cmd-planned.ts +15 -8
  47. package/src/cmd-routes.ts +27 -3
  48. package/src/cmd-secrets-ring.ts +76 -0
  49. package/src/cmd-secrets-spec.ts +10 -2
  50. package/src/cmd-secrets.ts +95 -18
  51. package/src/cmd-shot-matrix.ts +1 -3
  52. package/src/cmd-shot.ts +12 -3
  53. package/src/cmd-tasks.ts +84 -15
  54. package/src/cmd-test-spec.ts +1 -7
  55. package/src/cmd-test.ts +13 -29
  56. package/src/cmd-verify.ts +13 -5
  57. package/src/comment-safe.ts +16 -0
  58. package/src/coverage-floor.ts +334 -0
  59. package/src/coverage-lcov.ts +193 -0
  60. package/src/coverage-source.ts +136 -0
  61. package/src/db-object-drift.ts +30 -0
  62. package/src/db-replay-engine.ts +115 -0
  63. package/src/db-schema-dump.ts +150 -0
  64. package/src/db-schema-refresh.ts +122 -0
  65. package/src/db-subscribes.ts +12 -4
  66. package/src/dev-dashboard-guard.ts +76 -0
  67. package/src/dev-dashboard.ts +51 -29
  68. package/src/dev-lock.ts +4 -7
  69. package/src/dev-route-table.ts +9 -0
  70. package/src/dispatch.ts +3 -6
  71. package/src/doctor-app-url.ts +39 -0
  72. package/src/doctor-auth.ts +95 -0
  73. package/src/doctor-coverage.ts +39 -0
  74. package/src/doctor-guards.ts +54 -0
  75. package/src/doctor-sealed.ts +132 -0
  76. package/src/document-graph.ts +82 -0
  77. package/src/drift-replica-identity.ts +42 -0
  78. package/src/error-catalog.ts +6 -9
  79. package/src/error-codes.ts +55 -3
  80. package/src/error-unthrown.ts +29 -11
  81. package/src/errors.ts +54 -83
  82. package/src/framework-schema-apply.ts +78 -0
  83. package/src/framework-schema-stamp.ts +96 -0
  84. package/src/framework-schema.ts +28 -2
  85. package/src/generate-files.ts +79 -12
  86. package/src/generate-grant-findings.ts +76 -0
  87. package/src/generate-grants.ts +77 -3
  88. package/src/generate-kinds.ts +52 -1
  89. package/src/generate-shadow.ts +46 -0
  90. package/src/generate-write.ts +62 -40
  91. package/src/generated-imports.ts +59 -0
  92. package/src/gh-target.ts +12 -7
  93. package/src/guard-sources.ts +104 -0
  94. package/src/guards.ts +23 -7
  95. package/src/handle-registration.ts +217 -0
  96. package/src/i18n-audit.ts +4 -1
  97. package/src/i18n-registration.ts +36 -0
  98. package/src/image-prepare.ts +51 -8
  99. package/src/import-closure.ts +165 -0
  100. package/src/index.ts +12 -5
  101. package/src/island-bundle.ts +3 -15
  102. package/src/island-store.ts +24 -16
  103. package/src/jobs-drain.ts +95 -21
  104. package/src/jobs-json.ts +50 -0
  105. package/src/jobs-report.ts +7 -4
  106. package/src/json-merge.ts +46 -0
  107. package/src/loopback-host.ts +13 -0
  108. package/src/manifest-staleness.ts +62 -0
  109. package/src/mcp-errors.ts +41 -4
  110. package/src/mcp-host.ts +16 -9
  111. package/src/mcp-test-run.ts +38 -0
  112. package/src/messages.ts +35 -0
  113. package/src/metrics-endpoint.ts +95 -24
  114. package/src/migration-extensions.ts +29 -0
  115. package/src/module-imports.ts +47 -0
  116. package/src/output.ts +24 -9
  117. package/src/page-navigation.ts +7 -23
  118. package/src/page-speculation.ts +5 -20
  119. package/src/permission-grants.ts +41 -9
  120. package/src/prerender-out.ts +34 -7
  121. package/src/prerender.ts +8 -5
  122. package/src/publish-closure.ts +101 -0
  123. package/src/pwa-artifacts.ts +26 -94
  124. package/src/registry.ts +2 -0
  125. package/src/revalidated-response.ts +4 -3
  126. package/src/role-load.ts +143 -0
  127. package/src/role-replicator.ts +16 -0
  128. package/src/role-start.ts +27 -5
  129. package/src/role-sync.ts +55 -20
  130. package/src/role-wake.ts +45 -0
  131. package/src/runtime-admin.ts +141 -0
  132. package/src/runtime-assets.ts +11 -7
  133. package/src/runtime-bindings.ts +4 -4
  134. package/src/runtime-cache.ts +38 -47
  135. package/src/runtime-hooks.ts +5 -2
  136. package/src/runtime-idempotency-scope.ts +23 -0
  137. package/src/runtime-isr.ts +36 -0
  138. package/src/runtime-jobs.ts +8 -33
  139. package/src/runtime-mfa-warning.ts +29 -0
  140. package/src/runtime-notify-retention.ts +10 -46
  141. package/src/runtime-overrides.ts +1 -1
  142. package/src/runtime-purge.ts +38 -16
  143. package/src/runtime-queue.ts +39 -11
  144. package/src/runtime-realtime.ts +9 -45
  145. package/src/runtime-render.ts +68 -35
  146. package/src/runtime-services.ts +22 -18
  147. package/src/runtime-storage.ts +10 -5
  148. package/src/scaffold-typecheck.ts +1 -0
  149. package/src/schema-diff.ts +16 -0
  150. package/src/schema-drift.ts +45 -15
  151. package/src/schema-dump-drift.ts +64 -0
  152. package/src/seo-routes.ts +80 -9
  153. package/src/serve-boot.ts +114 -158
  154. package/src/serve-drain.ts +9 -33
  155. package/src/serve-env.ts +9 -6
  156. package/src/serve-prebuilt-paths.ts +19 -0
  157. package/src/serve-prebuilt.ts +84 -0
  158. package/src/serve-web.ts +169 -0
  159. package/src/serve.ts +6 -9
  160. package/src/server-barrels.ts +54 -0
  161. package/src/shot-locale.ts +5 -23
  162. package/src/site-assets.ts +3 -5
  163. package/src/site-config.ts +9 -48
  164. package/src/site-seo.ts +1 -1
  165. package/src/sitemap-lastmod.ts +5 -0
  166. package/src/solid-loader.ts +12 -3
  167. package/src/source-edit.ts +49 -0
  168. package/src/source-files.ts +6 -0
  169. package/src/stored-object-headers.ts +43 -0
  170. package/src/sync-url.ts +2 -2
  171. package/src/tasks-facts.ts +3 -26
  172. package/src/templates/action.ts +147 -11
  173. package/src/templates/admin-catalog.ts +58 -0
  174. package/src/templates/admin-page.ts +36 -8
  175. package/src/templates/admin.ts +57 -17
  176. package/src/templates/backfill.ts +121 -87
  177. package/src/templates/entity.ts +140 -44
  178. package/src/templates/guard-animated-layout-property.ts +4 -11
  179. package/src/templates/guard-bare-error.ts +9 -13
  180. package/src/templates/guard-focus-visible.ts +4 -11
  181. package/src/templates/guard-image-dimensions.ts +6 -6
  182. package/src/templates/guard-island-without-states.ts +2 -4
  183. package/src/templates/guard-raw-breakpoint.ts +210 -0
  184. package/src/templates/guard-raw-colour.ts +4 -11
  185. package/src/templates/guard-raw-length.ts +375 -0
  186. package/src/templates/guard-raw-motion.ts +213 -0
  187. package/src/templates/guard-raw-shadow.ts +187 -0
  188. package/src/templates/guard-raw-z-index.ts +166 -0
  189. package/src/templates/guard-repo-raw-sql.ts +345 -0
  190. package/src/templates/guard-semantic-interactive.ts +6 -6
  191. package/src/templates/guard-undeclared-custom-property.ts +217 -0
  192. package/src/templates/guard-undefined-style-class.ts +215 -0
  193. package/src/templates/guard-untranslated-string.ts +6 -6
  194. package/src/templates/guard-unzoned-date.ts +9 -13
  195. package/src/templates/index.ts +3 -2
  196. package/src/templates/island.ts +34 -51
  197. package/src/templates/job-body-test.ts +121 -0
  198. package/src/templates/job.ts +23 -8
  199. package/src/templates/naming.ts +28 -0
  200. package/src/templates/policy.ts +16 -9
  201. package/src/templates/query.ts +242 -26
  202. package/src/templates/resource-create.ts +62 -18
  203. package/src/templates/resource-form-island.ts +58 -80
  204. package/src/templates/resource-page.ts +269 -0
  205. package/src/templates/resource-service.ts +96 -0
  206. package/src/templates/resource.ts +86 -61
  207. package/src/templates/route.ts +41 -35
  208. package/src/templates/scaffold-api.ts +55 -1
  209. package/src/templates/scaffold-app.ts +177 -53
  210. package/src/templates/scaffold-auth.ts +83 -19
  211. package/src/templates/scaffold-browser-client.ts +90 -0
  212. package/src/templates/scaffold-container-compose.ts +10 -1
  213. package/src/templates/scaffold-container.ts +20 -2
  214. package/src/templates/scaffold-dashboard-bare.ts +64 -2
  215. package/src/templates/scaffold-dashboard-example.ts +124 -31
  216. package/src/templates/scaffold-dashboard-shared.ts +1 -20
  217. package/src/templates/scaffold-dashboard.ts +1 -2
  218. package/src/templates/scaffold-db-client.ts +144 -0
  219. package/src/templates/scaffold-db-package.ts +90 -8
  220. package/src/templates/scaffold-docs.ts +50 -5
  221. package/src/templates/scaffold-guards.ts +54 -11
  222. package/src/templates/scaffold-helm-fences.ts +76 -0
  223. package/src/templates/scaffold-helm-templates.ts +32 -7
  224. package/src/templates/scaffold-helm.ts +31 -1
  225. package/src/templates/scaffold-http.ts +9 -7
  226. package/src/templates/scaffold-repo.ts +61 -3
  227. package/src/templates/scaffold-roles.ts +56 -9
  228. package/src/templates/scaffold-shell.ts +74 -105
  229. package/src/templates/scaffold-site.ts +45 -19
  230. package/src/templates/scaffold-ui-package.ts +18 -0
  231. package/src/templates/slice-foundation.ts +46 -8
  232. package/src/test-counts.ts +14 -2
  233. package/src/test-ignores.ts +25 -0
  234. package/src/test-passes.ts +0 -15
  235. package/src/test-select.ts +5 -14
  236. package/src/test-shards.ts +41 -93
  237. package/src/test-workers.ts +26 -4
  238. package/src/theme-boot.ts +6 -31
  239. package/src/transport-calls.ts +105 -0
  240. package/src/trusted-hops-error.ts +17 -0
  241. package/src/unscanned-admin.ts +54 -0
  242. package/src/unthrown-codes.ts +26 -0
  243. package/src/verify-checks.ts +43 -53
  244. package/src/verify-coverage-run.ts +190 -0
  245. package/src/verify-deadline.ts +353 -0
  246. package/src/verify-floor.ts +36 -9
  247. package/src/verify-merge.ts +171 -45
  248. package/src/verify-part-step.ts +90 -0
  249. package/src/verify-progress.ts +25 -0
  250. package/src/verify-role-load-probe.ts +54 -0
  251. package/src/verify-role-load.ts +148 -0
  252. package/src/verify-run.ts +116 -37
  253. package/src/verify-shard.ts +33 -11
  254. package/src/verify-stalled.ts +152 -0
  255. package/src/verify-step.ts +42 -1
  256. package/src/verify-tests.ts +83 -8
  257. package/src/workspace-checks.ts +50 -8
  258. package/src/workspace-dep-edit.ts +127 -0
  259. package/src/build-errors.ts +0 -54
  260. package/src/cmd-dev-fixture.ts +0 -204
  261. package/src/dev-live-fixture.ts +0 -122
  262. package/src/policy-fixture.ts +0 -78
  263. package/src/realtime-browser-probe-fixture.ts +0 -9
  264. package/src/role-start-fixture.ts +0 -78
  265. package/src/scaffold-fixture.ts +0 -160
  266. package/src/templates/island-fixture.ts +0 -76
  267. package/src/verify-errors.ts +0 -49
@@ -0,0 +1,105 @@
1
+ // The four ways browser code opens its own connection, found in ONE file's text: a `fetch(` CALL,
2
+ // `new WebSocket(`, `new XMLHttpRequest(`, `new EventSource(`. Pure, so every false positive the
3
+ // rule has to leave alone is a fixture string. Which file may hold which shape is the caller's.
4
+
5
+ import { maskLiterals } from '@ultimat3/core';
6
+ import { lineIndex } from './ts-scan';
7
+
8
+ export type TransportShape = 'fetch' | 'websocket' | 'xhr' | 'eventsource';
9
+
10
+ export interface TransportCall {
11
+ readonly shape: TransportShape;
12
+ readonly line: number;
13
+ /** What the source spells — `fetch`, `globalThis.fetch`, `WebSocket` — so a finding quotes it. */
14
+ readonly spelled: string;
15
+ }
16
+
17
+ const CONSTRUCTED: ReadonlyMap<string, TransportShape> = new Map([
18
+ ['WebSocket', 'websocket'],
19
+ ['XMLHttpRequest', 'xhr'],
20
+ ['EventSource', 'eventsource'],
21
+ ]);
22
+
23
+ /** The global object's three spellings. `server.fetch(` is a method, never the global. */
24
+ const GLOBAL = String.raw`(?:(?:globalThis|window|self)\s*\.\s*)`;
25
+ const NEW = new RegExp(
26
+ String.raw`\bnew\s+${GLOBAL}?(WebSocket|XMLHttpRequest|EventSource)\s*\(`,
27
+ 'g',
28
+ );
29
+ const FETCH = new RegExp(String.raw`(?<![\w$.])(${GLOBAL}?)fetch\s*\(`, 'g');
30
+
31
+ /**
32
+ * Whether the file binds `fetch` itself — a `const`/`let`/`var`/`function`, an import, a
33
+ * destructured declaration or a parameter — which makes a BARE `fetch(` its own function, not the
34
+ * page's. `packages/action/src/idempotency-postgres.ts` calls a local `fetch(key)`. Per file, not
35
+ * per scope: a floor, and the one direction it errs is silence on a file that shadows the global
36
+ * AND calls it bare. `globalThis.fetch(` is reported regardless.
37
+ */
38
+ export function bindsFetch(code: string): boolean {
39
+ return (
40
+ /\b(?:const|let|var|function)\s+fetch\b/.test(code) ||
41
+ /\bimport\s+[^;]*\bfetch\b[^;]*\bfrom\b/.test(code) ||
42
+ /\b(?:const|let|var)\s*\{[^}]*\bfetch\b[^}]*\}\s*=/.test(code) ||
43
+ /\(\s*(?:[\w$]+\s*(?::[^,()]+)?,\s*)*fetch\s*[?:,)=][^()]*\)\s*(?::[^={;]+)?(?:=>|\{)/.test(
44
+ code,
45
+ )
46
+ );
47
+ }
48
+
49
+ /**
50
+ * The `)` closing the `(` at `open`, or `-1`. A bare depth count is exact HERE and only here: the
51
+ * text is `maskLiterals`' output, where every string, template, comment and regex body is already
52
+ * blank — so each parenthesis left is code.
53
+ */
54
+ function closeOf(masked: string, open: number): number {
55
+ let depth = 0;
56
+ for (let index = open; index < masked.length; index += 1) {
57
+ if (masked[index] === '(') depth += 1;
58
+ else if (masked[index] === ')') {
59
+ depth -= 1;
60
+ if (depth === 0) return index;
61
+ }
62
+ }
63
+ return -1;
64
+ }
65
+
66
+ /** `fetch(req) {` and `fetch(req): Promise<Response> {` define a method; they call nothing. */
67
+ function isDefinition(masked: string, open: number, start: number): boolean {
68
+ if (/\b(?:function|async|get|set)\s*$/.test(masked.slice(Math.max(0, start - 16), start))) {
69
+ return true;
70
+ }
71
+ const close = closeOf(masked, open);
72
+ if (close < 0) return false;
73
+ const after = masked.slice(close + 1).trimStart();
74
+ return after.startsWith('{') || (after.startsWith(':') && !after.startsWith('::'));
75
+ }
76
+
77
+ /** A file naming none of the four cannot call one — and masking it is the expensive half. */
78
+ const NAMES_A_TRANSPORT = /fetch|WebSocket|XMLHttpRequest|EventSource/;
79
+
80
+ export function transportCalls(source: string): readonly TransportCall[] {
81
+ if (!NAMES_A_TRANSPORT.test(source)) return [];
82
+ // Comments AND string contents blanked, offsets kept: `'fetch('` in a message is not a call, and
83
+ // neither is the one a template-literal scaffold emits for somebody else's file.
84
+ const masked = maskLiterals(source);
85
+ const lineAt = lineIndex(masked);
86
+ const calls: TransportCall[] = [];
87
+ for (const found of masked.matchAll(NEW)) {
88
+ const name = found[1] ?? '';
89
+ const shape = CONSTRUCTED.get(name);
90
+ if (shape !== undefined) calls.push({ shape, line: lineAt(found.index), spelled: name });
91
+ }
92
+ const local = bindsFetch(masked);
93
+ for (const found of masked.matchAll(FETCH)) {
94
+ const qualified = (found[1] ?? '').length > 0;
95
+ if (!qualified && local) continue;
96
+ const open = found.index + found[0].length - 1;
97
+ if (!qualified && isDefinition(masked, open, found.index)) continue;
98
+ calls.push({
99
+ shape: 'fetch',
100
+ line: lineAt(found.index),
101
+ spelled: qualified ? `${(found[1] ?? '').replace(/\s/g, '')}fetch` : 'fetch',
102
+ });
103
+ }
104
+ return calls.sort((a, b) => a.line - b.line);
105
+ }
@@ -0,0 +1,17 @@
1
+ // The refusal of a malformed `TRUSTED_PROXY_HOPS` — its own code, never `X_PORT_INVALID`: a proxy
2
+ // count is not a port, and an operator told "is not a TCP port number" looked for the wrong setting.
3
+
4
+ import { UltimateError } from '@ultimat3/core';
5
+ import { MAX_PROXY_HOPS } from '@ultimat3/http';
6
+ import { quoteArg } from './shell-quote';
7
+
8
+ export class TrustedProxyHopsInvalidError extends UltimateError {
9
+ constructor(input: { readonly value: string }) {
10
+ super({
11
+ code: 'X_TRUSTED_PROXY_HOPS_INVALID',
12
+ cause: `TRUSTED_PROXY_HOPS=${quoteArg(input.value)} is not a whole number of proxies from 1 to ${MAX_PROXY_HOPS} — the count that append to x-forwarded-for in front of this process; unset means none`,
13
+ fix: 'TRUSTED_PROXY_HOPS=1 ROLE=web bun apps/web/server.ts',
14
+ meta: { value: input.value, max: MAX_PROXY_HOPS },
15
+ });
16
+ }
17
+ }
@@ -0,0 +1,54 @@
1
+ // A `defineAdmin()` the app scan never imports. The admin's one home is `apps/admin/app/admin/`
2
+ // (`ADMIN_FILE`, what `x new` and `x g admin:page` write); `apps/<app>/src/` is outside every
3
+ // glob `app-load.ts` imports, so a declaration there never runs and `/admin` is never mounted —
4
+ // silently, which is how the reference app shipped an admin nobody could open.
5
+
6
+ import { renderFixShellArg, stripComments } from '@ultimat3/core';
7
+ import { describeRoutes } from '@ultimat3/render';
8
+ import { ADMIN_FILE } from './admin-registration';
9
+ import type { Finding } from './output';
10
+ import { hasPathSegment } from './path-segments';
11
+ import { isTest } from './source-files';
12
+
13
+ /** The one place an admin used to be written that the scan cannot see. */
14
+ const UNSCANNED = 'apps/*/src/**/*.{ts,tsx}';
15
+
16
+ const DECLARES = /\bdefineAdmin\(/;
17
+
18
+ /** Any admin this process mounted — then every declaration was reached, wherever it sits. */
19
+ export const adminMounted = (): boolean =>
20
+ describeRoutes().some((route) => route.mount?.by === 'defineAdmin');
21
+
22
+ /** Where the file belongs: its own name under the admin's home, `index.ts` as the declaration. */
23
+ const homeOf = (file: string): string => {
24
+ const name = file.slice(file.lastIndexOf('/') + 1);
25
+ const home = ADMIN_FILE.slice(0, ADMIN_FILE.lastIndexOf('/'));
26
+ return /^index\.tsx?$/.test(name) ? ADMIN_FILE : `${home}/${name}`;
27
+ };
28
+
29
+ const unscannedFinding = (file: string): Finding => ({
30
+ code: 'X_ADMIN_UNSCANNED',
31
+ cause: `${file} calls defineAdmin(), and the app scan imports apps/*/{site,app,api,shared}/** only — no boot evaluates it, so the admin is never mounted and /admin answers 404`,
32
+ // A file name is the app's: screened, so a name carrying shell syntax reads as a placeholder.
33
+ fix: `git mv ${renderFixShellArg(file, '<the file above>')} ${renderFixShellArg(homeOf(file), '<apps/admin/app/admin/its-name.ts>')} # then repoint its relative imports and x verify --only manifest --json`,
34
+ at: file,
35
+ });
36
+
37
+ /**
38
+ * One finding per unscanned file that declares an admin. Read only when nothing mounted one: an app
39
+ * whose admin is mounted pays for no glob, and comments are masked so prose naming the call is not
40
+ * a declaration.
41
+ */
42
+ export async function unscannedAdminFindings(
43
+ root: string,
44
+ mounted: () => boolean = adminMounted,
45
+ ): Promise<readonly Finding[]> {
46
+ if (mounted()) return [];
47
+ const files: string[] = [];
48
+ for await (const file of new Bun.Glob(UNSCANNED).scan({ cwd: root })) {
49
+ const path = file.replaceAll('\\', '/');
50
+ if (hasPathSegment(path, 'node_modules') || isTest(path)) continue;
51
+ if (DECLARES.test(stripComments(await Bun.file(`${root}/${path}`).text()))) files.push(path);
52
+ }
53
+ return files.sort().map(unscannedFinding);
54
+ }
@@ -0,0 +1,26 @@
1
+ // The registered codes this repo no longer throws, as a list `error-unthrown.ts` reads — its own
2
+ // file, so the literals naming the waived codes are not mistaken for a throw site.
3
+
4
+ /** Where the list lives, so a finding can name the file to edit. */
5
+ export const UNTHROWN_CODES_FILE = 'packages/cli/src/unthrown-codes.ts';
6
+
7
+ /**
8
+ * The codes this repo registers and no longer throws — each a decision, and each row on the
9
+ * reference page says so in words. A LIST, never a prose match: the waiver was any row containing
10
+ * "not thrown" or "thrown by nothing", so a sentence that happened to use either phrase silenced the
11
+ * rule for its code. A listing whose code is thrown again, or is not registered, is
12
+ * `X_ERROR_CODE_UNTHROWN_STALE`.
13
+ */
14
+ export const UNTHROWN_CODES: ReadonlySet<string> = new Set([
15
+ // `x db studio` is planned and unreleased; it exits X_NOT_IMPLEMENTED.
16
+ 'X_DB_STUDIO_FAILED',
17
+ // 21.0.0: there is no LiveClient to register (X_REALTIME_UNINSTALLED replaced it).
18
+ 'X_LIVE_CLIENT_MISSING',
19
+ // 21.0.0: liveHookFor was deleted; useQuery reads any query.
20
+ 'X_QUERY_NOT_SUBSCRIBABLE',
21
+ // 21.0.0: the worker no longer POSTs a flush endpoint nothing mounted.
22
+ 'X_PWA_SYNC_FLUSH_FAILED',
23
+ 'X_PWA_SYNC_INCOMPLETE',
24
+ // 24.0.0: `x test --worker` was deleted; `x verify --only unit --shard i/n` is the one split.
25
+ 'X_TEST_SHARD_FAILED',
26
+ ]);
@@ -6,18 +6,13 @@ import { existsSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { ERROR_DOCS_URL } from '@ultimat3/core';
8
8
  import type { Manifest } from '@ultimat3/manifest';
9
- import {
10
- AGENTS_MD_FILENAME,
11
- assertNoDrift,
12
- MANIFEST_FILENAME,
13
- verifyContract,
14
- } from '@ultimat3/manifest';
9
+ import { AGENTS_MD_FILENAME, MANIFEST_FILENAME, verifyContract } from '@ultimat3/manifest';
15
10
  import type { MetaIssue } from '@ultimat3/seo';
16
11
  import { validateMeta } from '@ultimat3/seo';
17
12
  import { checkAgentsMd } from './app-agents-md';
13
+ import { readAppSources } from './app-boundaries';
18
14
  import { envExampleFindings } from './app-env';
19
15
  import { appManifest, readAppManifest } from './app-manifest';
20
- import { OPENAPI_FILE, openApiStaleness } from './app-openapi';
21
16
  import { policyFindings } from './app-permissions';
22
17
  import { APP_CONFIG_FILE } from './app-root';
23
18
  import { asyncPageFindings } from './async-pages';
@@ -32,14 +27,21 @@ import { catalogFindings } from './i18n-registration';
32
27
  import { unregisteredJobFindings } from './job-registration';
33
28
  import { liveRouteFindings } from './live-routes';
34
29
  import { withLoadFindings, withoutLoadFindings } from './load-findings';
30
+ import {
31
+ hasCommittedContract,
32
+ manifestMissingFindings,
33
+ manifestStaleness,
34
+ } from './manifest-staleness';
35
35
  import { msg } from './messages';
36
36
  import type { Finding } from './output';
37
37
  import { findingFrom } from './output';
38
38
  import { checkMigrationDrift } from './schema-drift';
39
+ import { checkSchemaDump } from './schema-dump-drift';
39
40
  import { scanSiteMeta } from './seo-meta';
40
41
  import { quoteArg } from './shell-quote';
41
42
  import { readStaticReport } from './static-report';
42
43
  import { floorProblemFindings, readVerifyFloor } from './verify-floor';
44
+ import { roleLoadFindings } from './verify-role-load';
43
45
  import type { VerifyStep } from './verify-step';
44
46
  import { fromExec, fromFindings, hostFindings } from './verify-step';
45
47
  import { TEST_STEPS } from './verify-tests';
@@ -90,7 +92,8 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
90
92
  },
91
93
  {
92
94
  name: 'boundaries',
93
- summary: "surface, layer and package-tier imports, and the app's own guards",
95
+ summary:
96
+ "surface, layer and package-tier imports, one browser transport, and the app's own guards",
94
97
  // An app's `guards/` rides here rather than becoming a step of its own, for the reason the
95
98
  // seam already states: a host adds findings to a step, it can never add, remove, reorder or
96
99
  // skip one — so "green" keeps meaning exactly what it meant. This is the step whose host slot
@@ -101,12 +104,18 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
101
104
  // Discovered, not registered: `guardFindings` reads the directory. A guard that had to
102
105
  // announce itself is a guard an app can forget to announce, which is the coupling axiom 8's
103
106
  // extension model rejects.
104
- run: async (ctx) =>
105
- fromFindings([
106
- ...(await appBoundaryFindings(ctx.root)),
107
- ...(await guardFindings(ctx.root)),
107
+ //
108
+ // ONE read of the app's surface files for the whole step: the import rules, the transport
109
+ // rule and every guard are handed the same text. The guards used to walk and read the tree a
110
+ // second time (`guard-sources.ts`).
111
+ async run(ctx) {
112
+ const sources = await readAppSources(ctx.root);
113
+ return fromFindings([
114
+ ...(await appBoundaryFindings(ctx.root, sources)),
115
+ ...(await guardFindings(ctx.root, sources)),
108
116
  ...(await hostFindings(ctx, 'boundaries')),
109
- ]),
117
+ ]);
118
+ },
110
119
  },
111
120
  {
112
121
  name: 'filesize',
@@ -161,7 +170,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
161
170
  {
162
171
  name: 'drift',
163
172
  summary:
164
- 'entity declarations vs migrations, every destructive statement declared, and every statement no declaration carries',
173
+ 'entity declarations vs migrations, every destructive statement declared, every statement no declaration carries, and the committed schema dump',
165
174
  // Only an app owns migrations; a package monorepo's `packages/db` is the driver, not a schema.
166
175
  // Source, not database: the gate runs in CI with nothing listening, and the database half is
167
176
  // the post-migrate verification `runMigrations` performs where a connection is already open.
@@ -179,15 +188,20 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
179
188
  // a squash silently drops. Same directory, same reader, no database — this step's own
180
189
  // question, which is why it is not a step of its own.
181
190
  ...(await checkUngeneratableMigrations(ctx.root)),
191
+ // The fourth, and the only one that opens a database — a scratch embedded one it boots
192
+ // itself, so CI still needs nothing listening: the committed `packages/db/schema/` against
193
+ // what replaying the migrations produces, and that dump loaded back (`X_SCHEMA_DUMP_DRIFT`).
194
+ ...(await checkSchemaDump(ctx.root, ctx.env ?? Bun.env)),
182
195
  ]),
183
196
  },
184
197
  {
185
198
  name: 'contract-diff',
186
199
  summary: 'the published contract vs the committed manifest',
187
- // Either file is a published contract on its own: `openapi.json` generates the typed client,
188
- // so gating on the manifest alone let a stale spec ship a wrong client unchecked.
189
- applies: async (ctx) =>
190
- existsSync(join(ctx.root, MANIFEST_FILENAME)) || existsSync(join(ctx.root, OPENAPI_FILE)),
200
+ // A BREAKING change against the committed manifest, and nothing else. Whether the committed
201
+ // files are stale — `openapi.json` included — is the `manifest` step's, through the one check
202
+ // `x manifest --check` runs (`manifest-staleness.ts`): reported here as well it was two
203
+ // steps and one command answering one question, and the step named `manifest` saying green.
204
+ applies: async (ctx) => existsSync(join(ctx.root, MANIFEST_FILENAME)),
191
205
  async run(ctx) {
192
206
  const committed = await readAppManifest(ctx.root);
193
207
  const { manifest, findings } = await appManifest(ctx.root);
@@ -195,14 +209,13 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
195
209
  return withoutLoadFindings(ctx.root, [
196
210
  ...findings,
197
211
  ...(committed === undefined ? [] : contractFindings(committed, manifest)),
198
- ...(await openApiStaleness(ctx.root, manifest)),
199
212
  ]);
200
213
  },
201
214
  },
202
215
  {
203
216
  name: 'budgets',
204
217
  summary:
205
- 'per-route JS bytes and LCP, the global style layer every document carries, and the routes that boot nothing to receive their live rows',
218
+ 'per-route JS bytes, the global style layer every document carries, and the routes that boot nothing to receive their live rows',
206
219
  // The global-style assertion rides here rather than becoming a step of its own, because this
207
220
  // step already asks the one question it asks: what does the document this build emits actually
208
221
  // contain? It is also the same app load — `appManifest` fills render's stylesheet registry on
@@ -328,14 +341,18 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
328
341
  const agents = await checkAgentsMd(ctx.root, floor?.agentsMdMaxBytes);
329
342
  // The app's load failures, once, here — every other step that loaded it points at this one.
330
343
  const app = existsSync(join(ctx.root, APP_CONFIG_FILE));
344
+ // Started first and awaited last: it is a child process, and it runs beside the rest.
345
+ const roleLoad = app ? roleLoadFindings(ctx.root, ctx.runner) : Promise.resolve([]);
331
346
  const drift = await driftFindings(ctx.root);
332
347
  const findings = [
333
- ...manifestMissingFindings(ctx.root),
334
348
  ...(app ? await withLoadFindings(ctx.root, drift) : drift),
335
349
  // An app only: the framework monorepo declares no `defineApi`, so it has no list to be in.
336
350
  ...(existsSync(join(ctx.root, APP_CONFIG_FILE))
337
351
  ? await unregisteredJobFindings(ctx.root)
338
352
  : []),
353
+ // What a worker never registers: a declaration beside a component the API index does not
354
+ // import (`verify-role-load.ts`). The boot checks jobs and tasks only, and only at boot.
355
+ ...(await roleLoad),
339
356
  ...(await envExampleFindings(ctx.root)),
340
357
  ...floorProblemFindings(floor),
341
358
  ...agents.findings,
@@ -365,41 +382,14 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
365
382
  ];
366
383
 
367
384
  /**
368
- * The half that had never been asked: is the file there at all? `driftFindings` returns nothing
369
- * when it is absent — correctly, it has nothing to compare — and `AGENTS.md` line 3 tells an agent
370
- * that facts live in `x.manifest.json`, `x dev` prints its path, and `x manifest` is the only
371
- * thing that writes it. Nothing ran it: after `x new`, `bin/setup` and all thirteen generators,
372
- * `find . -name '*.manifest.json'` returned nothing while this step reported green (#F7).
373
- *
374
- * An app root, never this repo: the framework monorepo emits `framework.manifest.json` and has no
375
- * `x.manifest.json` to be missing, so the `app.config.ts` is what decides — the same discriminator
376
- * `drift`, `budgets`, `seo`, `i18n` and `policy` each use.
385
+ * The committed generated files against the code — `x manifest --check`'s check, through its
386
+ * function. With no committed contract there is nothing to load an app for: an app root is told
387
+ * its manifest is missing, and the framework monorepo (which has neither file) is told nothing.
377
388
  */
378
- function manifestMissingFindings(root: string): readonly Finding[] {
379
- if (!existsSync(join(root, APP_CONFIG_FILE))) return [];
380
- if (existsSync(join(root, MANIFEST_FILENAME))) return [];
381
- return [
382
- {
383
- code: 'X_MANIFEST_MISSING',
384
- cause: `${MANIFEST_FILENAME} does not exist, so every fact an agent reads about this app — route table, action schemas, policies, error codes — is unavailable`,
385
- fix: 'x manifest',
386
- docs: ERROR_DOCS_URL,
387
- at: MANIFEST_FILENAME,
388
- },
389
- ];
390
- }
391
-
392
- /** `assertNoDrift` throws `X_MANIFEST_DRIFT`; a step reports, so the error becomes a finding. */
393
389
  async function driftFindings(root: string): Promise<readonly Finding[]> {
394
- const path = join(root, MANIFEST_FILENAME);
395
- if (!existsSync(path)) return [];
390
+ if (!hasCommittedContract(root)) return manifestMissingFindings(root);
396
391
  const { manifest, findings } = await appManifest(root);
397
- try {
398
- await assertNoDrift({ manifest, path });
399
- return findings;
400
- } catch (error) {
401
- return [...findings, { ...findingFrom(error), at: MANIFEST_FILENAME }];
402
- }
392
+ return [...findings, ...(await manifestStaleness(root, manifest))];
403
393
  }
404
394
 
405
395
  /** A breaking change is allowed — with a major bump. `verifyContract` is the one that decides. */
@@ -0,0 +1,190 @@
1
+ // An app's `unit` step, run so its coverage can be read: the suite as a pool of plain `bun test`
2
+ // processes over fixed slices, each writing its own lcov, folded by `coverage-lcov.ts`'s rule.
3
+ //
4
+ // NOT `bun test --parallel` with `--coverage` added, which is the obvious form and was measured
5
+ // first (2026-10-01, Bun 1.4.0, `examples/dummy`, 53 unit files): Bun's own cross-worker merge is
6
+ // a union, so the SAME tree read 59.9% of lines at four workers, 65.0% at two and 65.5% in one
7
+ // process. A floor that moves with the core count is not a floor. Per-process lcov folded here
8
+ // read 65.90% at 2, 4 and 8 processes.
9
+ //
10
+ // It costs nothing measurable: four slice processes took 9.7 s against 12.4 s for `--parallel=4`
11
+ // over the same files, on a loaded 12-core box.
12
+
13
+ // why: Bun ships no recursive delete and no path-join primitive; the lcov parts live under `.x/`.
14
+ import { rm } from 'node:fs/promises';
15
+ // why: Bun ships no path-join primitive.
16
+ import { dirname, join } from 'node:path';
17
+ import type { CoverageMap } from './coverage-lcov';
18
+ import { fileCoverageOf, mergeCoverage, parseLcov } from './coverage-lcov';
19
+ import type { ExecResult, Runner } from './exec';
20
+ import { execOutput } from './exec';
21
+ import { msg } from './messages';
22
+ import { countsOf } from './test-counts';
23
+ import { testEnvOverrides } from './test-dotenv';
24
+ import { failureOf, ISOLATED_TEST_ENV } from './test-shards';
25
+ import { SLOT_HELD_ENV } from './test-slots';
26
+ import type { StepOutcome } from './verify-step';
27
+
28
+ /**
29
+ * Test files per process. Small enough that a process is thrown away before its heap grows with
30
+ * the corpus (`test-batches.ts`), large enough that the ~0.3 s a process costs to start is noise.
31
+ */
32
+ export const COVERAGE_SLICE_FILES = 16;
33
+
34
+ /** Where a run's lcov parts are written, under the app root — removed when the run has read them. */
35
+ export const COVERAGE_DIR = '.x/coverage';
36
+
37
+ /**
38
+ * The slices one run is spent as: a pure function of the sorted file list, never of the machine,
39
+ * so the measured number is the same on a laptop and on a runner. A directory's tests stay in one
40
+ * slice — function coverage is the best one process reached (`mergeFileCoverage`), and the tests
41
+ * that cover a file sit beside it — and only a directory larger than the cap is cut.
42
+ */
43
+ export function coverageSlices(
44
+ files: readonly string[],
45
+ cap: number = COVERAGE_SLICE_FILES,
46
+ ): readonly (readonly string[])[] {
47
+ const byDir = new Map<string, string[]>();
48
+ for (const file of [...new Set(files)].sort()) {
49
+ const dir = dirname(file);
50
+ const list = byDir.get(dir) ?? [];
51
+ list.push(file);
52
+ byDir.set(dir, list);
53
+ }
54
+ const slices: string[][] = [];
55
+ let open: string[] = [];
56
+ const close = (): void => {
57
+ if (open.length > 0) slices.push(open);
58
+ open = [];
59
+ };
60
+ for (const group of byDir.values()) {
61
+ if (group.length > cap) {
62
+ close();
63
+ for (let at = 0; at < group.length; at += cap) slices.push(group.slice(at, at + cap));
64
+ } else {
65
+ if (open.length + group.length > cap) close();
66
+ open.push(...group);
67
+ }
68
+ }
69
+ close();
70
+ return slices;
71
+ }
72
+
73
+ export interface CoveredRunOptions {
74
+ readonly root: string;
75
+ readonly runner: Runner;
76
+ /** Paths relative to `root`. */
77
+ readonly files: readonly string[];
78
+ readonly workers: number;
79
+ readonly isolate?: boolean;
80
+ readonly env?: Readonly<Record<string, string | undefined>>;
81
+ readonly widthReason?: string;
82
+ readonly lease?: (want: number) => Promise<{ readonly count: number; release(): void }>;
83
+ /** Test files per process. Absent is `COVERAGE_SLICE_FILES`; a test cuts smaller. */
84
+ readonly sliceFiles?: number;
85
+ }
86
+
87
+ export interface CoveredRun extends StepOutcome {
88
+ /** Every file any slice loaded, by the path Bun wrote — relative to `root`. */
89
+ readonly coverage: CoverageMap;
90
+ }
91
+
92
+ /** One slice's argv. An explicit file list, never a re-glob, for `testArgs`' reason. */
93
+ export const coveredArgs = (
94
+ files: readonly string[],
95
+ dir: string,
96
+ isolate: boolean,
97
+ ): readonly string[] => [
98
+ 'bun',
99
+ 'test',
100
+ ...(isolate ? ['--isolate'] : []),
101
+ '--coverage',
102
+ '--coverage-reporter=lcov',
103
+ `--coverage-dir=${dir}`,
104
+ ...files,
105
+ ];
106
+
107
+ export async function runCovered(options: CoveredRunOptions): Promise<CoveredRun> {
108
+ const slices = coverageSlices(options.files, options.sliceFiles);
109
+ const want = Math.max(1, Math.min(Math.trunc(options.workers), slices.length));
110
+ const lease = want > 1 ? await options.lease?.(want) : undefined;
111
+ const width = Math.min(want, lease?.count ?? want);
112
+ const base = join(options.root, COVERAGE_DIR, `unit-${String(process.pid)}`);
113
+ const isolate = options.isolate === true;
114
+ const overrides = {
115
+ ...testEnvOverrides(options.root, options.env ?? Bun.env),
116
+ ...(isolate ? { [ISOLATED_TEST_ENV]: '1' } : {}),
117
+ ...(lease === undefined ? {} : { [SLOT_HELD_ENV]: '1' }),
118
+ };
119
+ const results = new Array<ExecResult>(slices.length);
120
+ let next = 0;
121
+ // Each pool worker names its own numbered test database, as `--parallel`'s workers do.
122
+ const worker = async (slot: number): Promise<void> => {
123
+ while (next < slices.length) {
124
+ const index = next;
125
+ next += 1;
126
+ results[index] = await options.runner(
127
+ coveredArgs(slices[index] as readonly string[], join(base, String(index)), isolate),
128
+ { cwd: options.root, env: { ...overrides, ULTIMATE_TEST_WORKER: String(slot) } },
129
+ );
130
+ }
131
+ };
132
+ let maps: CoverageMap[];
133
+ try {
134
+ await Promise.all(Array.from({ length: width }, (_, slot) => worker(slot)));
135
+ maps = await Promise.all(slices.map((_, index) => readPart(join(base, String(index)))));
136
+ } finally {
137
+ lease?.release();
138
+ await rm(base, { recursive: true, force: true });
139
+ }
140
+ const failed = results.filter((result) => !result.ok);
141
+ const first = failed[0];
142
+ const output = results
143
+ .map((result, index) =>
144
+ result.ok
145
+ ? undefined
146
+ : [
147
+ msg('cli.test.batch', {
148
+ batch: index + 1,
149
+ batches: results.length,
150
+ files: slices[index]?.length ?? 0,
151
+ }),
152
+ execOutput(result),
153
+ ].join('\n'),
154
+ )
155
+ .filter((text) => text !== undefined)
156
+ .join('\n');
157
+ return {
158
+ ok: first === undefined,
159
+ findings:
160
+ first === undefined
161
+ ? []
162
+ : [
163
+ failureOf(first.code, options.files.length, {
164
+ workers: width,
165
+ type: 'unit',
166
+ ...(isolate ? { isolate: true } : {}),
167
+ }),
168
+ ],
169
+ workers: width,
170
+ ...(options.widthReason === undefined ? {} : { widthReason: options.widthReason }),
171
+ tests: countsOf(results),
172
+ ...(first === undefined ? {} : { output }),
173
+ coverage: mergeCoverage(maps),
174
+ };
175
+ }
176
+
177
+ /**
178
+ * One slice's lcov as a map, kept to the app's own tree: a slice loads a thousand framework files
179
+ * for every hundred of the app's, and none of those is what the floor measures. No file is no
180
+ * coverage — the slice died before writing it, and its exit code already says so.
181
+ */
182
+ async function readPart(dir: string): Promise<CoverageMap> {
183
+ const file = Bun.file(join(dir, 'lcov.info'));
184
+ if (!(await file.exists())) return {};
185
+ return Object.fromEntries(
186
+ parseLcov(await file.text())
187
+ .filter((record) => /^(?:apps|packages)\//.test(record.file))
188
+ .map((record) => [record.file, fileCoverageOf(record)]),
189
+ );
190
+ }