@ultimat3/cli 23.0.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 (149) hide show
  1. package/CLAUDE.md +15 -7
  2. package/README.md +3 -3
  3. package/package.json +28 -28
  4. package/src/affected.ts +27 -35
  5. package/src/api-registration.ts +89 -0
  6. package/src/app-artifacts.ts +1 -1
  7. package/src/app-auth.ts +4 -27
  8. package/src/app-config-load.ts +51 -0
  9. package/src/app-env.ts +5 -11
  10. package/src/app-manifest.ts +2 -8
  11. package/src/app-mcp.ts +10 -23
  12. package/src/app-permissions-borrowed.ts +129 -0
  13. package/src/app-permissions-site.ts +24 -0
  14. package/src/app-permissions.ts +7 -17
  15. package/src/budgets.ts +4 -35
  16. package/src/cdp-shot-driver.ts +3 -1
  17. package/src/cmd-auth-spec.ts +16 -0
  18. package/src/cmd-auth.ts +65 -0
  19. package/src/cmd-build.ts +64 -15
  20. package/src/cmd-db-backfill.ts +25 -12
  21. package/src/cmd-db.ts +6 -5
  22. package/src/cmd-deploy-helm.ts +7 -6
  23. package/src/cmd-deploy.ts +4 -4
  24. package/src/cmd-dev.ts +9 -8
  25. package/src/cmd-doctor-spec.ts +2 -9
  26. package/src/cmd-doctor.ts +41 -14
  27. package/src/cmd-env.ts +26 -2
  28. package/src/cmd-generate.ts +79 -32
  29. package/src/cmd-help.ts +19 -6
  30. package/src/cmd-i18n.ts +10 -9
  31. package/src/cmd-jobs.ts +18 -6
  32. package/src/cmd-manifest-spec.ts +1 -1
  33. package/src/cmd-manifest.ts +6 -25
  34. package/src/cmd-mcp.ts +46 -15
  35. package/src/cmd-planned.ts +15 -8
  36. package/src/cmd-routes.ts +12 -2
  37. package/src/cmd-secrets.ts +47 -6
  38. package/src/cmd-shot-matrix.ts +1 -3
  39. package/src/cmd-shot.ts +12 -3
  40. package/src/cmd-tasks.ts +2 -2
  41. package/src/cmd-test-spec.ts +1 -7
  42. package/src/cmd-test.ts +13 -29
  43. package/src/cmd-verify.ts +4 -4
  44. package/src/db-subscribes.ts +12 -4
  45. package/src/dev-dashboard-guard.ts +76 -0
  46. package/src/dev-dashboard.ts +51 -29
  47. package/src/dev-lock.ts +4 -7
  48. package/src/dispatch.ts +3 -6
  49. package/src/doctor-app-url.ts +39 -0
  50. package/src/doctor-auth.ts +95 -0
  51. package/src/drift-replica-identity.ts +42 -0
  52. package/src/error-catalog.ts +6 -9
  53. package/src/error-codes.ts +23 -3
  54. package/src/error-unthrown.ts +29 -11
  55. package/src/errors.ts +54 -83
  56. package/src/framework-schema-apply.ts +78 -0
  57. package/src/framework-schema-stamp.ts +96 -0
  58. package/src/framework-schema.ts +13 -2
  59. package/src/generate-files.ts +4 -3
  60. package/src/generate-kinds.ts +33 -1
  61. package/src/generate-shadow.ts +46 -0
  62. package/src/generate-write.ts +47 -13
  63. package/src/gh-target.ts +12 -7
  64. package/src/i18n-audit.ts +4 -1
  65. package/src/index.ts +1 -3
  66. package/src/island-bundle.ts +3 -15
  67. package/src/jobs-drain.ts +82 -15
  68. package/src/jobs-report.ts +4 -4
  69. package/src/loopback-host.ts +13 -0
  70. package/src/manifest-staleness.ts +62 -0
  71. package/src/mcp-errors.ts +12 -3
  72. package/src/mcp-host.ts +16 -9
  73. package/src/mcp-test-run.ts +38 -0
  74. package/src/messages.ts +2 -0
  75. package/src/metrics-endpoint.ts +25 -1
  76. package/src/output.ts +6 -0
  77. package/src/page-navigation.ts +7 -23
  78. package/src/page-speculation.ts +5 -20
  79. package/src/prerender-out.ts +34 -7
  80. package/src/prerender.ts +8 -5
  81. package/src/pwa-artifacts.ts +26 -94
  82. package/src/registry.ts +2 -0
  83. package/src/revalidated-response.ts +4 -3
  84. package/src/role-replicator.ts +16 -0
  85. package/src/role-start.ts +9 -4
  86. package/src/role-sync.ts +55 -20
  87. package/src/runtime-assets.ts +11 -7
  88. package/src/runtime-bindings.ts +4 -4
  89. package/src/runtime-cache.ts +38 -47
  90. package/src/runtime-idempotency-scope.ts +23 -0
  91. package/src/runtime-isr.ts +36 -0
  92. package/src/runtime-jobs.ts +8 -33
  93. package/src/runtime-mfa-warning.ts +29 -0
  94. package/src/runtime-notify-retention.ts +10 -46
  95. package/src/runtime-overrides.ts +1 -1
  96. package/src/runtime-purge.ts +38 -16
  97. package/src/runtime-queue.ts +31 -10
  98. package/src/runtime-realtime.ts +9 -45
  99. package/src/runtime-render.ts +62 -35
  100. package/src/runtime-services.ts +20 -17
  101. package/src/runtime-storage.ts +10 -5
  102. package/src/schema-diff.ts +16 -0
  103. package/src/schema-drift.ts +45 -15
  104. package/src/seo-routes.ts +80 -9
  105. package/src/serve-boot.ts +34 -6
  106. package/src/serve-drain.ts +9 -33
  107. package/src/serve-env.ts +9 -6
  108. package/src/serve-web.ts +12 -7
  109. package/src/shot-locale.ts +5 -23
  110. package/src/site-assets.ts +3 -5
  111. package/src/site-config.ts +9 -48
  112. package/src/site-seo.ts +1 -1
  113. package/src/sitemap-lastmod.ts +5 -0
  114. package/src/solid-loader.ts +12 -3
  115. package/src/source-files.ts +6 -0
  116. package/src/stored-object-headers.ts +43 -0
  117. package/src/sync-url.ts +2 -2
  118. package/src/tasks-facts.ts +3 -29
  119. package/src/templates/action.ts +3 -0
  120. package/src/templates/naming.ts +28 -0
  121. package/src/templates/route.ts +3 -6
  122. package/src/templates/scaffold-app.ts +6 -2
  123. package/src/templates/scaffold-container-compose.ts +10 -1
  124. package/src/templates/scaffold-container.ts +4 -2
  125. package/src/templates/scaffold-db-client.ts +4 -2
  126. package/src/templates/scaffold-helm-fences.ts +76 -0
  127. package/src/templates/scaffold-helm-templates.ts +32 -7
  128. package/src/templates/scaffold-helm.ts +31 -1
  129. package/src/templates/scaffold-repo.ts +9 -1
  130. package/src/templates/scaffold-shell.ts +39 -36
  131. package/src/test-counts.ts +2 -6
  132. package/src/test-ignores.ts +25 -0
  133. package/src/test-passes.ts +0 -15
  134. package/src/test-select.ts +5 -14
  135. package/src/test-shards.ts +41 -93
  136. package/src/test-workers.ts +26 -4
  137. package/src/theme-boot.ts +6 -31
  138. package/src/trusted-hops-error.ts +17 -0
  139. package/src/unthrown-codes.ts +26 -0
  140. package/src/verify-checks.ts +17 -46
  141. package/src/verify-deadline.ts +130 -33
  142. package/src/verify-merge.ts +60 -27
  143. package/src/verify-part-step.ts +90 -0
  144. package/src/verify-run.ts +29 -8
  145. package/src/verify-shard.ts +13 -2
  146. package/src/verify-stalled.ts +152 -0
  147. package/src/verify-tests.ts +5 -2
  148. package/src/build-errors.ts +0 -54
  149. package/src/verify-errors.ts +0 -58
@@ -39,12 +39,12 @@
39
39
  // `@ultimat3/testing`'s preload clears the plugins after each file (`isolated-plugins.ts`). And the
40
40
  // per-WORKER database survives untouched: `@ultimat3/testing`'s `workerId` already read
41
41
  // `BUN_TEST_WORKER_ID` as its second key, which is exactly what Bun sets, 1..N, one per real
42
- // process (probed on 1.4.0). `ULTIMATE_TEST_WORKER` stays the first key and is what `--worker`
43
- // still sets, so a single-shard rerun keeps naming its own database.
42
+ // process (probed on 1.4.0).
44
43
 
45
44
  // why: Bun ships no path-join primitive; the timings cache lives under the run's root.
46
45
  import { join } from 'node:path';
47
46
  import { ERROR_DOCS_URL } from '@ultimat3/core';
47
+ import { ISOLATED_ENV } from '@ultimat3/testing';
48
48
  import type { AffectedSelection } from './affected';
49
49
  import type { Runner } from './exec';
50
50
  import { msg } from './messages';
@@ -52,8 +52,8 @@ import type { CommandResult, Finding, JsonValue, StepResult } from './output';
52
52
  import { quoteArg } from './shell-quote';
53
53
  import { testEnvOverrides } from './test-dotenv';
54
54
 
55
- /** `@ultimat3/testing`'s `ISOLATED_ENV`, restated: `cli → testing` is a runtime edge, kept to fixtures. */
56
- export const ISOLATED_TEST_ENV = 'ULTIMATE_TEST_ISOLATED';
55
+ /** `@ultimat3/testing`'s `ISOLATED_ENV` — the one spelling, which that package's preload reads. */
56
+ export const ISOLATED_TEST_ENV = ISOLATED_ENV;
57
57
 
58
58
  /** Bun's per-file durations, cached under the run's root and refreshed by every parallel run. */
59
59
  export const TEST_TIMINGS_FILE = '.x/test-timings.json';
@@ -68,24 +68,13 @@ import type { TestType } from './verify-tests';
68
68
  * The argv for one run. An explicit file list, never a re-glob: discovery already decided which
69
69
  * files belong to this type, and a child that globs again can pick up a file the selection removed.
70
70
  *
71
- * `--parallel=N` for the whole selection, `--shard=i+1/N` for one slice of it. The shard form is
72
- * `--worker`'s, and it carries `--isolate` in its own right — only `--parallel` implies it, and a
73
- * partition without a fresh module registry per file is the failure mode this whole design exists
74
- * to remove: measured on this repo, an 8-way split turned 0 failures into 36, every one
75
- * `X_PERMISSION_UNKNOWN` in `@ultimat3/query` because the `packages/cli` file that had been
76
- * declaring `feed:read` for it landed elsewhere. Half a dozen registries here are process-global by
77
- * design — the permission set, the roles, the entity/action/query tables, the error-code titles,
78
- * the fixture bag — and a serial `bun test` only passes because glob order happens to put every
79
- * declaring file before every file that reads what it left behind.
80
- *
81
- * Bun's shard partition is round-robin over the list it is given, so the sorted list this hands it
82
- * makes `--shard=2/8` the same 1/8 on CI and on a laptop (probed on 1.4.0).
71
+ * `--parallel=N` over the selection, sorted, so two runs over one corpus hand Bun the same list.
72
+ * One CI job's share of the corpus is `x verify --only unit --shard i/n` (`verify-shard.ts`), never
73
+ * a flag here: `x test --worker I` was a second, 0-based, unhashed split of the same suite.
83
74
  */
84
75
  export function testArgs(input: {
85
76
  readonly files: readonly string[];
86
77
  readonly workers: number;
87
- /** 0-based, matching `--worker`. Absent runs the whole selection across `workers` processes. */
88
- readonly shard?: number;
89
78
  /**
90
79
  * Everything after a bare `--`, handed to `bun test` verbatim and BEFORE the file list, which is
91
80
  * where bun reads its flags. `ParsedArgs.passthrough` had no reader anywhere until 2026-09, so
@@ -114,32 +103,23 @@ export function testArgs(input: {
114
103
  const isolate =
115
104
  extra.includes('--isolate') || (input.isolate === true && !extra.includes('--no-isolate'));
116
105
  const bare = extra.filter((arg) => arg !== '--isolate' && arg !== '--no-isolate');
117
- return input.shard === undefined
118
- ? [
119
- 'bun',
120
- 'test',
121
- `--parallel=${String(input.workers)}`,
122
- ...(isolate ? [] : ['--no-isolate']),
123
- ...(input.timings === undefined || bare.some((arg) => arg.startsWith('--timings'))
124
- ? []
125
- : [`--timings=${input.timings}`, '--update-timings']),
126
- ...bare,
127
- ...files,
128
- ]
129
- : [
130
- 'bun',
131
- 'test',
132
- ...(isolate ? ['--isolate'] : []),
133
- `--shard=${String(input.shard + 1)}/${String(input.workers)}`,
134
- ...bare,
135
- ...files,
136
- ];
106
+ return [
107
+ 'bun',
108
+ 'test',
109
+ `--parallel=${String(input.workers)}`,
110
+ ...(isolate ? [] : ['--no-isolate']),
111
+ ...(input.timings === undefined || bare.some((arg) => arg.startsWith('--timings'))
112
+ ? []
113
+ : [`--timings=${input.timings}`, '--update-timings']),
114
+ ...bare,
115
+ ...files,
116
+ ];
137
117
  }
138
118
 
139
119
  /**
140
120
  * The files an argv selects, flags stripped. `bun test` takes its file list positionally, so this
141
121
  * is the inverse of `testArgs` and the one thing a test asserting "what did the child get?" needs
142
- * — the flag COUNT is not fixed (`--parallel=N` is one token, a shard run carries two more), and a
122
+ * — the flag COUNT is not fixed (`--no-isolate` and the timings pair come and go), and a
143
123
  * test slicing a hardcoded prefix length reads a flag as a filename the day that changes.
144
124
  */
145
125
  export const filesIn = (command: readonly string[]): readonly string[] =>
@@ -154,12 +134,9 @@ export interface ReproduceOptions {
154
134
  readonly sample?: number;
155
135
  /**
156
136
  * The `--affected` narrowing, when there was one. The input most easily forgotten: `--affected`
157
- * decides which files exist to run at all, so a rerun without it selects the WHOLE corpus and
158
- * its shard 2 is a different shard 2.
137
+ * decides which files exist to run at all, so a rerun without it selects the WHOLE corpus.
159
138
  */
160
139
  readonly affected?: AffectedSelection;
161
- /** 0-based, when reproducing ONE shard. Absent reproduces the whole selection. */
162
- readonly shard?: number;
163
140
  /** What the caller put after `--`. It reaches `bun test`, so a rerun without it runs differently. */
164
141
  readonly passthrough?: readonly string[];
165
142
  /** The run was isolated per file; the rerun must be too. */
@@ -185,7 +162,6 @@ export function reproduceFor(options: ReproduceOptions): string {
185
162
  ...(options.affected?.dirty === true ? ['--dirty'] : []),
186
163
  '--workers',
187
164
  String(options.workers),
188
- ...(options.shard === undefined ? [] : ['--worker', String(options.shard)]),
189
165
  ...(options.isolate === true ? ['--isolate'] : []),
190
166
  // Last, and after a `--` of its own, because that is where the caller typed it and where the
191
167
  // parser will find it again. Quoted for `shell-quote.ts`'s reason: a reproduce line is pasted.
@@ -200,14 +176,11 @@ export interface RunShardsOptions {
200
176
  readonly runner: Runner;
201
177
  readonly files: readonly TestFile[];
202
178
  readonly workers: number;
203
- /** Run exactly one shard of the same N-way split, not a one-worker run of everything. */
204
- readonly only?: number;
205
179
  readonly filter?: string;
206
180
  readonly type?: TestType;
207
181
  /**
208
182
  * Set when `--sample` narrowed `files`: `kept` is what survived, `total` what discovery found.
209
- * `kept` is carried rather than counted from what ran, because `--worker N` runs one shard of the
210
- * sample and would otherwise report that shard's size as the corpus.
183
+ * Carried rather than counted from what ran: a pass runs part of the sample, never the corpus.
211
184
  */
212
185
  readonly sample?: { readonly kept: number; readonly total: number };
213
186
  /** Passed straight to `reproduceFor`: see `ReproduceOptions.affected`. */
@@ -248,7 +221,6 @@ const planOf = (
248
221
  ...(pass.type === undefined ? {} : { type: pass.type }),
249
222
  ...(options.sample === undefined ? {} : { sample: options.sample.kept }),
250
223
  ...(options.affected === undefined ? {} : { affected: options.affected }),
251
- ...(options.only === undefined ? {} : { shard: options.only }),
252
224
  ...(options.isolate === true ? { isolate: true } : {}),
253
225
  ...(options.passthrough === undefined || options.passthrough.length === 0
254
226
  ? {}
@@ -256,17 +228,12 @@ const planOf = (
256
228
  });
257
229
 
258
230
  /**
259
- * `X_TEST_SHARD_FAILED` for a `--worker` run and `X_TEST_FAILED` for a whole one, because the two
260
- * name different reruns: a shard is reproduced by naming it, and a full run by rerunning it. Both
261
- * codes already exist and both are already documented — a third would be a new name for a failed
262
- * `bun test`.
231
+ * `X_TEST_FAILED`, whose `fix:` reruns exactly what failed. `X_TEST_SHARD_FAILED` was the
232
+ * `--worker` rerun's and went with it: the code stays registered (codes are forever) and unthrown.
263
233
  */
264
234
  export const failureOf = (code: number, files: number, plan: ReproduceOptions): Finding => ({
265
- code: plan.shard === undefined ? 'X_TEST_FAILED' : 'X_TEST_SHARD_FAILED',
266
- cause:
267
- plan.shard === undefined
268
- ? `${plan.type ?? 'test'} run exited ${code} across ${plan.workers} worker(s) (${files} file(s))`
269
- : `shard ${plan.shard} of ${plan.workers} exited ${code} (${files} file(s))`,
235
+ code: 'X_TEST_FAILED',
236
+ cause: `${plan.type ?? 'test'} run exited ${code} across ${plan.workers} worker(s) (${files} file(s))`,
270
237
  fix: reproduceFor(plan),
271
238
  docs: ERROR_DOCS_URL,
272
239
  });
@@ -281,12 +248,10 @@ export const failureOf = (code: number, files: number, plan: ReproduceOptions):
281
248
  * it. And every pass runs even after one fails — the caller asked for a suite, and a report that
282
249
  * stops at the first red step hides the rest of the answer.
283
250
  *
284
- * `ULTIMATE_TEST_WORKER` is still set for a `--worker` rerun and only then: that run is one
285
- * process, so naming its database is this file's to do. A `--parallel` run has N of them and Bun
286
- * numbers each with `BUN_TEST_WORKER_ID`, which `@ultimat3/testing`'s `workerId` already reads.
251
+ * No `ULTIMATE_TEST_WORKER` here: a `--parallel` run has N processes and Bun numbers each with
252
+ * `BUN_TEST_WORKER_ID`, which `@ultimat3/testing`'s `workerId` already reads.
287
253
  */
288
254
  export async function runShards(options: RunShardsOptions): Promise<CommandResult> {
289
- const only = options.only;
290
255
  // Computed ONCE per invocation, never per pass: every pass spawns from the same `root` and the
291
256
  // same parent env, so the leaked-key set cannot differ pass to pass.
292
257
  const envOverrides: Record<string, string | undefined> = {
@@ -302,7 +267,6 @@ export async function runShards(options: RunShardsOptions): Promise<CommandResul
302
267
  files: options.files,
303
268
  workers: options.workers,
304
269
  ...(options.type === undefined ? {} : { type: options.type }),
305
- ...(only === undefined ? {} : { shard: only }),
306
270
  });
307
271
  const started = performance.now();
308
272
  const steps: StepResult[] = [];
@@ -312,53 +276,38 @@ export async function runShards(options: RunShardsOptions): Promise<CommandResul
312
276
  let exitCode = 0;
313
277
  for (const pass of passes) {
314
278
  const files = pass.files.map((file) => file.path);
315
- // A `--worker` rerun is one process over one shard and is never split: shard i of the rerun
316
- // must be the same files as shard i of the run it reproduces. Nor is `-- --watch`, which never
317
- // exits, so a second batch would never start. Every other pass is spent in `test-batches.ts`'
318
- // batches, one `bun test` after another.
319
- const batches =
320
- only === undefined && !watching
321
- ? testBatches(
322
- files,
323
- pass.workers,
324
- isolatedRun(options) ? BATCH_FILES_PER_WORKER : SHARED_BATCH_FILES_PER_WORKER,
325
- )
326
- : [files];
279
+ // `-- --watch` is never split: it never exits, so a second batch would never start. Every
280
+ // other pass is spent in `test-batches.ts`' batches, one `bun test` after another.
281
+ const batches = !watching
282
+ ? testBatches(
283
+ files,
284
+ pass.workers,
285
+ isolatedRun(options) ? BATCH_FILES_PER_WORKER : SHARED_BATCH_FILES_PER_WORKER,
286
+ )
287
+ : [files];
327
288
  const result = await runBatches({
328
289
  runner: options.runner,
329
290
  batches,
330
291
  stopOnFailure: bailing,
331
292
  workers: pass.workers,
332
- // A `--worker` rerun is one process whatever its N says, and a serial pass is one worker:
333
- // only a default-width parallel pass leases from the machine pool.
334
- ...(only === undefined && pass.workers > 1 && options.lease !== undefined
335
- ? { lease: options.lease }
336
- : {}),
293
+ // A serial pass is one worker: only a default-width parallel pass leases from the pool.
294
+ ...(pass.workers > 1 && options.lease !== undefined ? { lease: options.lease } : {}),
337
295
  argsFor: (batch, width) =>
338
296
  testArgs({
339
297
  files: batch,
340
- workers: only === undefined ? width : pass.workers,
341
- ...(only === undefined ? {} : { shard: only }),
298
+ workers: width,
342
299
  ...(options.passthrough === undefined ? {} : { passthrough: options.passthrough }),
343
300
  ...(options.isolate === undefined ? {} : { isolate: options.isolate }),
344
301
  timings: join(options.root, TEST_TIMINGS_FILE),
345
302
  }),
346
303
  options: {
347
304
  cwd: options.root,
348
- ...(Object.keys(envOverrides).length === 0 && only === undefined
349
- ? {}
350
- : {
351
- env: {
352
- ...envOverrides,
353
- ...(only === undefined ? {} : { ULTIMATE_TEST_WORKER: String(only) }),
354
- },
355
- }),
305
+ ...(Object.keys(envOverrides).length === 0 ? {} : { env: envOverrides }),
356
306
  },
357
307
  });
358
308
  batchCount += batches.length;
359
309
  const plan = planOf(options, pass);
360
- const label =
361
- only === undefined ? `${pass.workers} worker(s)` : `shard ${only} of ${pass.workers}`;
310
+ const label = `${pass.workers} worker(s)`;
362
311
  steps.push({
363
312
  name: `${pass.type === undefined ? label : `${pass.type} · ${label}`} · ${files.length} files${
364
313
  batches.length > 1 ? msg('cli.test.batches', { batches: batches.length }) : ''
@@ -403,7 +352,6 @@ export async function runShards(options: RunShardsOptions): Promise<CommandResul
403
352
  durationMs,
404
353
  ...(options.filter === undefined ? {} : { filter: options.filter }),
405
354
  ...(sample === undefined ? {} : { sample: { kept: sample.kept, total: sample.total } }),
406
- ...(only === undefined ? {} : { shard: only }),
407
355
  // Only when the split made more than one, so a single-pass run's JSON is byte-identical to
408
356
  // what it has always been — and a mixed one can never be read as if it were a single run.
409
357
  ...(spent.length > 1 ? { passes: spent } : {}),
@@ -1,14 +1,36 @@
1
1
  // How wide a parallel test run goes, decided in one place. `x test --workers`, `x verify
2
2
  // --workers` and every parallel step of the gate read this — a second default would split the same
3
- // suite two different ways, and the `--worker N` reproduction a shard failure prints would then
4
- // name a shard the gate never ran.
3
+ // suite two different ways, and the `--workers N` a failure's reproduction prints would then name
4
+ // a width the gate never ran.
5
5
 
6
6
  // Bun ships no CPU-count or memory primitive: `cpus()` is the fallback when navigator cannot
7
7
  // answer, and `totalmem()` is the only reader of the machine's RAM.
8
8
  // why: Bun ships no CPU-count or total-memory primitive.
9
9
  import { cpus, totalmem } from 'node:os';
10
+ import { UltimateError } from '@ultimat3/core';
10
11
  import type { TestType } from '@ultimat3/testing';
11
- import { TestBudgetInvalidError } from './verify-errors';
12
+
13
+ /**
14
+ * `ULTIMATE_TEST_MEMORY_BUDGET` or `ULTIMATE_TEST_MAX_WORKERS` set to something that does not
15
+ * parse. Refused rather than ignored: a typo read as "the default" is a run sized for a machine
16
+ * the caller said this is not.
17
+ */
18
+ export class TestBudgetInvalidError extends UltimateError {
19
+ constructor(name: string, value: string) {
20
+ super({
21
+ code: 'X_TEST_BUDGET_INVALID',
22
+ cause: `${name}=${JSON.stringify(value)} does not parse — ${
23
+ name === 'ULTIMATE_TEST_MAX_WORKERS'
24
+ ? 'expected a positive integer'
25
+ : 'expected a size such as 3g, 512m or 4GiB'
26
+ }`,
27
+ fix:
28
+ name === 'ULTIMATE_TEST_MAX_WORKERS'
29
+ ? 'ULTIMATE_TEST_MAX_WORKERS=4 x verify'
30
+ : 'ULTIMATE_TEST_MEMORY_BUDGET=3g x verify',
31
+ });
32
+ }
33
+ }
12
34
 
13
35
  /** navigator first: it is the runtime's own answer, and it respects a container's CPU limit. */
14
36
  export function availableCpus(): number {
@@ -219,7 +241,7 @@ export const sharedWorkers = (
219
241
  * Which types run across worker processes, and why the other two cannot.
220
242
  *
221
243
  * Parallel is safe when the only thing a test file shares with another file is the database, and
222
- * the database is per worker by construction (`ULTIMATE_TEST_WORKER` → one clone of the migrated
244
+ * the database is per worker by construction (`BUN_TEST_WORKER_ID` → one clone of the migrated
223
245
  * template, `@ultimat3/testing`'s `acquireWorkerDatabase`). Every other process-global in this
224
246
  * framework — the permission set, the roles, the entity/action/query registries, the error-code
225
247
  * titles, the fixture bag — is handled by `--isolate` giving each FILE its own module registry.
package/src/theme-boot.ts CHANGED
@@ -1,40 +1,15 @@
1
- // The no-flash theme script, inlined into every document a served or exported process writes,
2
- // with `theme.defaultMode` from `app.config.ts` as its fallback. Before this the key had no reader
3
- // at all (pinned as a dead key in `scripts/lib/config-reader-pins.ts`) and neither of the two
4
- // scripts the framework exported was wired into a document: an app that wanted to open dark had to
5
- // write its own boot script AND admit it to the CSP by hand — which every app forgot in one of the
6
- // two places. Sibling of `app-auth.ts`'s `loadSignInPath` and imports the config for the same
7
- // reason: a regex over the app's source is the pattern `app-load.ts` refuses.
1
+ // The no-flash theme script, inlined into every document a served or exported process writes, with
2
+ // `theme.defaultMode` (off the one loader, `app-config-load.ts`) as its fallback — and the CSP source
3
+ // that admits it, so no app writes its own boot script and forgets to admit it.
8
4
 
9
- // why: Bun exposes no path-join primitive, and the config path is app-root-relative — the same
10
- // necessity `app-auth.ts` records.
11
- import { join } from 'node:path';
12
5
  import type { ThemeMode } from '@ultimat3/core';
13
6
  import { cspHashSource } from '@ultimat3/http';
14
7
  import { renderHead, themeScript, themeScriptBody } from '@ultimat3/render';
15
- import { APP_CONFIG_EXPORT } from './app-auth';
16
- import { APP_CONFIG_FILE } from './app-root';
8
+ import { loadAppConfig } from './app-config-load';
17
9
 
18
- const isRecord = (value: unknown): value is Record<string, unknown> =>
19
- typeof value === 'object' && value !== null;
20
-
21
- const isThemeMode = (value: unknown): value is ThemeMode =>
22
- value === 'light' || value === 'dark' || value === 'system';
23
-
24
- /**
25
- * `theme.defaultMode`, or `'system'` — the value `defineConfig` fills in — when the app declares
26
- * none or the file is absent. Structural, never `instanceof`, for `loadSignInPath`'s reason.
27
- */
10
+ /** `theme.defaultMode`, or `'system'` — core's own default — for a root with no config file. */
28
11
  export async function loadThemeMode(root: string): Promise<ThemeMode> {
29
- const configPath = join(root, APP_CONFIG_FILE);
30
- if (!(await Bun.file(configPath).exists())) return 'system';
31
- const module = (await import(configPath)) as Record<string, unknown>;
32
- const config = module[APP_CONFIG_EXPORT];
33
- if (!isRecord(config)) return 'system';
34
- const theme = config['theme'];
35
- if (!isRecord(theme)) return 'system';
36
- const mode = theme['defaultMode'];
37
- return isThemeMode(mode) ? mode : 'system';
12
+ return (await loadAppConfig(root))?.theme.defaultMode ?? 'system';
38
13
  }
39
14
 
40
15
  export interface ThemeBoot {
@@ -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,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,19 +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';
18
13
  import { readAppSources } from './app-boundaries';
19
14
  import { envExampleFindings } from './app-env';
20
15
  import { appManifest, readAppManifest } from './app-manifest';
21
- import { OPENAPI_FILE, openApiStaleness } from './app-openapi';
22
16
  import { policyFindings } from './app-permissions';
23
17
  import { APP_CONFIG_FILE } from './app-root';
24
18
  import { asyncPageFindings } from './async-pages';
@@ -33,6 +27,11 @@ import { catalogFindings } from './i18n-registration';
33
27
  import { unregisteredJobFindings } from './job-registration';
34
28
  import { liveRouteFindings } from './live-routes';
35
29
  import { withLoadFindings, withoutLoadFindings } from './load-findings';
30
+ import {
31
+ hasCommittedContract,
32
+ manifestMissingFindings,
33
+ manifestStaleness,
34
+ } from './manifest-staleness';
36
35
  import { msg } from './messages';
37
36
  import type { Finding } from './output';
38
37
  import { findingFrom } from './output';
@@ -198,10 +197,11 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
198
197
  {
199
198
  name: 'contract-diff',
200
199
  summary: 'the published contract vs the committed manifest',
201
- // Either file is a published contract on its own: `openapi.json` generates the typed client,
202
- // so gating on the manifest alone let a stale spec ship a wrong client unchecked.
203
- applies: async (ctx) =>
204
- 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)),
205
205
  async run(ctx) {
206
206
  const committed = await readAppManifest(ctx.root);
207
207
  const { manifest, findings } = await appManifest(ctx.root);
@@ -209,14 +209,13 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
209
209
  return withoutLoadFindings(ctx.root, [
210
210
  ...findings,
211
211
  ...(committed === undefined ? [] : contractFindings(committed, manifest)),
212
- ...(await openApiStaleness(ctx.root, manifest)),
213
212
  ]);
214
213
  },
215
214
  },
216
215
  {
217
216
  name: 'budgets',
218
217
  summary:
219
- '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',
220
219
  // The global-style assertion rides here rather than becoming a step of its own, because this
221
220
  // step already asks the one question it asks: what does the document this build emits actually
222
221
  // contain? It is also the same app load — `appManifest` fills render's stylesheet registry on
@@ -346,7 +345,6 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
346
345
  const roleLoad = app ? roleLoadFindings(ctx.root, ctx.runner) : Promise.resolve([]);
347
346
  const drift = await driftFindings(ctx.root);
348
347
  const findings = [
349
- ...manifestMissingFindings(ctx.root),
350
348
  ...(app ? await withLoadFindings(ctx.root, drift) : drift),
351
349
  // An app only: the framework monorepo declares no `defineApi`, so it has no list to be in.
352
350
  ...(existsSync(join(ctx.root, APP_CONFIG_FILE))
@@ -384,41 +382,14 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
384
382
  ];
385
383
 
386
384
  /**
387
- * The half that had never been asked: is the file there at all? `driftFindings` returns nothing
388
- * when it is absent — correctly, it has nothing to compare — and `AGENTS.md` line 3 tells an agent
389
- * that facts live in `x.manifest.json`, `x dev` prints its path, and `x manifest` is the only
390
- * thing that writes it. Nothing ran it: after `x new`, `bin/setup` and all thirteen generators,
391
- * `find . -name '*.manifest.json'` returned nothing while this step reported green (#F7).
392
- *
393
- * An app root, never this repo: the framework monorepo emits `framework.manifest.json` and has no
394
- * `x.manifest.json` to be missing, so the `app.config.ts` is what decides — the same discriminator
395
- * `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.
396
388
  */
397
- function manifestMissingFindings(root: string): readonly Finding[] {
398
- if (!existsSync(join(root, APP_CONFIG_FILE))) return [];
399
- if (existsSync(join(root, MANIFEST_FILENAME))) return [];
400
- return [
401
- {
402
- code: 'X_MANIFEST_MISSING',
403
- cause: `${MANIFEST_FILENAME} does not exist, so every fact an agent reads about this app — route table, action schemas, policies, error codes — is unavailable`,
404
- fix: 'x manifest',
405
- docs: ERROR_DOCS_URL,
406
- at: MANIFEST_FILENAME,
407
- },
408
- ];
409
- }
410
-
411
- /** `assertNoDrift` throws `X_MANIFEST_DRIFT`; a step reports, so the error becomes a finding. */
412
389
  async function driftFindings(root: string): Promise<readonly Finding[]> {
413
- const path = join(root, MANIFEST_FILENAME);
414
- if (!existsSync(path)) return [];
390
+ if (!hasCommittedContract(root)) return manifestMissingFindings(root);
415
391
  const { manifest, findings } = await appManifest(root);
416
- try {
417
- await assertNoDrift({ manifest, path });
418
- return findings;
419
- } catch (error) {
420
- return [...findings, { ...findingFrom(error), at: MANIFEST_FILENAME }];
421
- }
392
+ return [...findings, ...(await manifestStaleness(root, manifest))];
422
393
  }
423
394
 
424
395
  /** A breaking change is allowed — with a major bump. `verifyContract` is the one that decides. */