@ultimat3/cli 2.0.0 → 4.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 (75) hide show
  1. package/CLAUDE.md +109 -13
  2. package/README.md +1 -0
  3. package/package.json +24 -24
  4. package/src/budgets.ts +31 -8
  5. package/src/cmd-db-branch.ts +6 -2
  6. package/src/cmd-db.ts +138 -10
  7. package/src/cmd-deploy.ts +42 -14
  8. package/src/cmd-dev.ts +9 -2
  9. package/src/cmd-docs.ts +7 -3
  10. package/src/cmd-doctor.ts +16 -7
  11. package/src/cmd-fix.ts +15 -3
  12. package/src/cmd-generate.ts +29 -4
  13. package/src/cmd-help.ts +25 -4
  14. package/src/cmd-i18n.ts +8 -5
  15. package/src/cmd-jobs.ts +6 -5
  16. package/src/cmd-mcp.ts +16 -12
  17. package/src/cmd-new.ts +10 -14
  18. package/src/cmd-planned.ts +13 -0
  19. package/src/cmd-policy.ts +8 -6
  20. package/src/cmd-registries.ts +7 -6
  21. package/src/cmd-routes.ts +27 -4
  22. package/src/cmd-secrets.ts +6 -6
  23. package/src/cmd-test.ts +14 -3
  24. package/src/cmd-verify.ts +80 -10
  25. package/src/command.ts +10 -2
  26. package/src/db-branch.ts +18 -0
  27. package/src/db-generate.ts +38 -6
  28. package/src/db-seed.ts +294 -0
  29. package/src/dev-assets.ts +22 -3
  30. package/src/dev-cache.ts +9 -9
  31. package/src/dev-render.ts +6 -1
  32. package/src/dev-roles.ts +5 -3
  33. package/src/dev-runtime.ts +2 -2
  34. package/src/dev-storage.ts +6 -4
  35. package/src/dev-traces.ts +26 -4
  36. package/src/dispatch.ts +33 -4
  37. package/src/drift.ts +41 -1
  38. package/src/error-catalog.ts +1 -0
  39. package/src/error-codes.ts +11 -0
  40. package/src/error-contract.ts +31 -4
  41. package/src/exec.ts +42 -8
  42. package/src/fix-command.ts +9 -2
  43. package/src/fix-imports.ts +118 -0
  44. package/src/fix-scan.ts +251 -0
  45. package/src/flag-number.ts +11 -0
  46. package/src/flag-reads.ts +114 -0
  47. package/src/i18n-audit.ts +2 -1
  48. package/src/index.ts +19 -5
  49. package/src/jobs-drain.ts +6 -1
  50. package/src/mcp-errors.ts +13 -0
  51. package/src/mcp-host.ts +4 -2
  52. package/src/messages.ts +15 -0
  53. package/src/metrics-endpoint.ts +60 -13
  54. package/src/otlp-export.ts +14 -0
  55. package/src/parse.ts +6 -1
  56. package/src/seo-meta.ts +105 -0
  57. package/src/serve.ts +15 -3
  58. package/src/shell-quote.ts +15 -0
  59. package/src/templates/action.ts +39 -7
  60. package/src/templates/backfill.ts +3 -1
  61. package/src/templates/index.ts +10 -1
  62. package/src/templates/job.ts +6 -2
  63. package/src/templates/query.ts +6 -1
  64. package/src/templates/route.ts +18 -9
  65. package/src/templates/scaffold-api.ts +100 -0
  66. package/src/templates/scaffold-app.ts +8 -48
  67. package/src/templates/scaffold-container.ts +44 -9
  68. package/src/templates/scaffold-helm-templates.ts +327 -0
  69. package/src/templates/scaffold-helm.ts +144 -0
  70. package/src/templates/scaffold-repo.ts +25 -8
  71. package/src/test-shards.ts +1 -10
  72. package/src/test-workers.ts +4 -1
  73. package/src/ts-scan.ts +25 -176
  74. package/src/tsconfig-references.ts +27 -2
  75. package/src/verify-step.ts +5 -0
package/src/db-seed.ts ADDED
@@ -0,0 +1,294 @@
1
+ // `x db seed`, everything except the argv: where a seed is declared, which tier this environment
2
+ // takes, and what one pass reports. A driver plus plain strings in, plain rows out — the
3
+ // `db-backfill.ts` split repeated, so every rule here is testable with no `ParsedArgs` and no boot.
4
+ //
5
+ // The decisions a seed itself owns are `@ultimat3/entity`'s: `seedTiersFor` is the one table saying
6
+ // which tiers an environment runs, and two copies of "may this seed run" would be two answers.
7
+
8
+ // `node:path` for the joiner and the app-root-relative spelling every finding is keyed by; Bun
9
+ // exposes neither.
10
+ import { relative, sep } from 'node:path';
11
+ import type { Environment } from '@ultimat3/core';
12
+ import { UltimateError } from '@ultimat3/core';
13
+ import type { Driver, Seed, SeedTier } from '@ultimat3/entity';
14
+ import { isSeed, SEED_TIERS, seedTiersFor } from '@ultimat3/entity';
15
+ import { docsFor } from './error-codes';
16
+ import { BadFlagError } from './errors';
17
+ import type { Finding, JsonValue } from './output';
18
+ import { findingFrom } from './output';
19
+ import { renderTable } from './table';
20
+
21
+ /**
22
+ * Where an app keeps seeds: a `seeds` directory in a package, or a `seed*.ts` beside its entities —
23
+ * the two layouts the tracked apps already use, and nothing wider. `loadApp`'s whole-src glob was
24
+ * the alternative and it is the wrong tool here: importing every module of every package to find a
25
+ * fixture graph makes an unrelated module that will not import into a failed seed run.
26
+ * `apps/` is deliberately absent: a fixture graph is data, and data lives in a package.
27
+ */
28
+ export const SEED_GLOBS = ['packages/*/seeds/**/*.ts', 'packages/*/src/seed*.ts'] as const;
29
+
30
+ /**
31
+ * `x db seed <name>` named a seed no module declared. `X_DECLARATION_UNKNOWN` is the code the
32
+ * registries already answer this with — a seed is a declaration, and a second code for "no such
33
+ * name" is the synonym the registry exists to prevent. The known names ARE listed, unlike
34
+ * `DeclarationUnknownError`'s count: an app has one to five seeds, not two hundred actions, and
35
+ * picking another one is the entire remedy.
36
+ *
37
+ * Both classes live HERE rather than in `errors.ts` for one reason, stated so nobody has to guess:
38
+ * that file is at the 500-line ceiling `x verify`'s `filesize` step enforces, and these two are
39
+ * `x db seed`'s alone. The codes stay CLI-owned in `error-codes.ts`, as every code does.
40
+ */
41
+ export class SeedUnknownError extends UltimateError {
42
+ constructor(input: { name: string; known: readonly string[] }) {
43
+ super({
44
+ code: 'X_DECLARATION_UNKNOWN',
45
+ cause:
46
+ input.known.length === 0
47
+ ? `no seed named "${input.name}" — this app declares none (a seed is an exported defineSeed() in packages/<pkg>/seeds or packages/<pkg>/src/seed.ts)`
48
+ : `no seed named "${input.name}" is declared (known: ${input.known.join(', ')})`,
49
+ // A dry run, never a bare `x db seed`: the command that answers "which seeds are there" must
50
+ // not be the command that writes them.
51
+ fix: 'x db seed --dry-run --json',
52
+ docs: docsFor('X_DECLARATION_UNKNOWN'),
53
+ });
54
+ }
55
+ }
56
+
57
+ /**
58
+ * The seed's tier is not one this environment runs. `dev` fixtures reaching production is the one
59
+ * irreversible mistake `x db seed` can make, so it is refused rather than confirmed.
60
+ *
61
+ * `X_SEED_ENVIRONMENT` is its own code, not `X_CLI_BAD_FLAG`: the argv was well formed and the
62
+ * answer is still no. A flag code says "you typed it wrong" and sends the reader to `x help`; this
63
+ * says "this environment does not run that tier", whose one remedy is naming the tier. The env var
64
+ * is named in the cause and not in the `fix:`, because a `fix:` is one pasteable line and a
65
+ * container with a fixed command line is the case that needs the other half.
66
+ */
67
+ export class SeedEnvironmentError extends UltimateError {
68
+ constructor(input: {
69
+ seed: string;
70
+ tier: string;
71
+ environment: string;
72
+ tiers: readonly string[];
73
+ }) {
74
+ super({
75
+ code: 'X_SEED_ENVIRONMENT',
76
+ cause: `seed "${input.seed}" is tier ${input.tier} and ULTIMATE_ENV resolved ${input.environment}, where x db seed runs ${input.tiers.join(', ')} — ULTIMATE_SEED_TIER=${input.tier} says this deploy takes it anyway`,
77
+ fix: `x db seed ${input.seed} --tier ${input.tier} --json`,
78
+ docs: docsFor('X_SEED_ENVIRONMENT'),
79
+ });
80
+ }
81
+ }
82
+
83
+ export interface DiscoveredSeed {
84
+ readonly seed: Seed;
85
+ /** App-root-relative POSIX path of the module that declared it. */
86
+ readonly file: string;
87
+ }
88
+
89
+ export interface SeedDiscovery {
90
+ readonly seeds: readonly DiscoveredSeed[];
91
+ /** Modules that would not import. Reported, never swallowed: one of them may hold the seed. */
92
+ readonly findings: readonly Finding[];
93
+ }
94
+
95
+ /**
96
+ * Every seed the app declares, by importing the modules that declare them — the same rule
97
+ * `loadApp` follows, because importing IS the declaration. Sorted by file, so a run's order is the
98
+ * one a reader can predict from the tree (`01_orgs.ts` before `02_posts.ts`) rather than the one a
99
+ * glob happened to yield.
100
+ */
101
+ export async function discoverSeeds(root: string): Promise<SeedDiscovery> {
102
+ const seeds: DiscoveredSeed[] = [];
103
+ const findings: Finding[] = [];
104
+ const seen = new Set<string>();
105
+ for (const pattern of SEED_GLOBS) {
106
+ for await (const absolute of new Bun.Glob(pattern).scan({ cwd: root, absolute: true })) {
107
+ if (absolute.includes('node_modules') || absolute.includes('.test.')) continue;
108
+ if (seen.has(absolute)) continue;
109
+ seen.add(absolute);
110
+ const file = relative(root, absolute).split(sep).join('/');
111
+ let module: Record<string, unknown>;
112
+ try {
113
+ module = (await import(absolute)) as Record<string, unknown>;
114
+ } catch (error) {
115
+ findings.push({ ...findingFrom(error), at: file });
116
+ continue;
117
+ }
118
+ for (const value of Object.values(module)) {
119
+ if (isSeed(value)) seeds.push({ seed: value, file });
120
+ }
121
+ }
122
+ }
123
+ return {
124
+ seeds: seeds.toSorted((left, right) => left.file.localeCompare(right.file)),
125
+ findings,
126
+ };
127
+ }
128
+
129
+ /** `--tier`, or `ULTIMATE_SEED_TIER` for a container whose command line is fixed. */
130
+ export function parseSeedTierFlag(value: string | undefined): SeedTier | undefined {
131
+ if (value === undefined || value === '') return undefined;
132
+ if ((SEED_TIERS as readonly string[]).includes(value)) return value as SeedTier;
133
+ throw new BadFlagError({
134
+ flag: 'tier',
135
+ command: 'db seed',
136
+ reason: `unknown tier "${value}" (known: ${SEED_TIERS.join(', ')})`,
137
+ fix: 'x db seed --dry-run --json',
138
+ });
139
+ }
140
+
141
+ export interface SeedSelection {
142
+ readonly discovered: readonly DiscoveredSeed[];
143
+ /** The positional. Absent runs every seed whose tier this environment takes. */
144
+ readonly name?: string | undefined;
145
+ readonly environment: Environment;
146
+ readonly requested?: SeedTier | undefined;
147
+ }
148
+
149
+ /**
150
+ * Which seeds this invocation runs, and the two refusals that are not a run.
151
+ *
152
+ * The environment check is HERE and again in `cmd-db.ts` before the driver is booted, on purpose:
153
+ * seeding is the one irreversible thing this command does, and the layer that boots a connection
154
+ * to production must not be the only layer that decided it was allowed to.
155
+ */
156
+ export function selectSeeds(input: SeedSelection): readonly DiscoveredSeed[] {
157
+ const tiers = seedTiersFor(input.environment, input.requested);
158
+ const known = input.discovered.map((entry) => entry.seed.name);
159
+ if (input.name === undefined) {
160
+ return input.discovered.filter((entry) => tiers.includes(entry.seed.tier));
161
+ }
162
+ const chosen = input.discovered.filter((entry) => entry.seed.name === input.name);
163
+ const first = chosen[0];
164
+ if (first === undefined) throw new SeedUnknownError({ name: input.name, known });
165
+ if (chosen.length > 1) {
166
+ throw new BadFlagError({
167
+ flag: 'name',
168
+ command: 'db seed',
169
+ reason: `"${input.name}" names ${chosen.length} seeds (${chosen.map((entry) => entry.file).join(', ')}) — a seed name is how a run is asked for, so two of them make the ask unanswerable`,
170
+ fix: 'x db seed --dry-run --json',
171
+ });
172
+ }
173
+ if (!tiers.includes(first.seed.tier)) {
174
+ throw new SeedEnvironmentError({
175
+ seed: first.seed.name,
176
+ tier: first.seed.tier,
177
+ environment: input.environment,
178
+ tiers,
179
+ });
180
+ }
181
+ return chosen;
182
+ }
183
+
184
+ export type SeedStatus = 'ok' | 'failed';
185
+
186
+ export interface SeedPassRow {
187
+ readonly file: string;
188
+ readonly name: string;
189
+ readonly tier: SeedTier;
190
+ readonly status: SeedStatus;
191
+ readonly ms: number;
192
+ readonly inserted: number;
193
+ readonly updated: number;
194
+ readonly skipped: number;
195
+ readonly finding: Finding | null;
196
+ }
197
+
198
+ export interface SeedPassOptions {
199
+ readonly seeds: readonly DiscoveredSeed[];
200
+ readonly driver: Driver;
201
+ readonly dryRun: boolean;
202
+ readonly env?: Readonly<Record<string, string | undefined>> | undefined;
203
+ /**
204
+ * One transaction PER SEED, never one around the run: a seed that fails must not roll back the
205
+ * ones that already succeeded, and a fixture graph half-written is worse than one not written.
206
+ * Injected so this stays testable with no database; `cmd-db.ts` passes `withTransaction`.
207
+ */
208
+ readonly transaction: <T>(work: () => Promise<T>) => Promise<T>;
209
+ }
210
+
211
+ /** Each seed, in file order, each isolated from the next. Never throws — a failure is a row. */
212
+ export async function runSeeds(options: SeedPassOptions): Promise<readonly SeedPassRow[]> {
213
+ const rows: SeedPassRow[] = [];
214
+ for (const entry of options.seeds) {
215
+ const started = Bun.nanoseconds();
216
+ const elapsed = (): number => Math.round((Bun.nanoseconds() - started) / 1_000_000);
217
+ try {
218
+ const run = await options.transaction(() =>
219
+ entry.seed.run({ driver: options.driver, dryRun: options.dryRun, env: options.env }),
220
+ );
221
+ rows.push({
222
+ file: entry.file,
223
+ name: run.name,
224
+ tier: run.tier,
225
+ status: 'ok',
226
+ ms: elapsed(),
227
+ ...run.metrics,
228
+ finding: null,
229
+ });
230
+ } catch (error) {
231
+ rows.push({
232
+ file: entry.file,
233
+ name: entry.seed.name,
234
+ tier: entry.seed.tier,
235
+ status: 'failed',
236
+ ms: elapsed(),
237
+ inserted: 0,
238
+ updated: 0,
239
+ skipped: 0,
240
+ finding: { ...findingFrom(error), at: entry.file },
241
+ });
242
+ }
243
+ }
244
+ return rows;
245
+ }
246
+
247
+ export interface SeedTotals {
248
+ readonly inserted: number;
249
+ readonly updated: number;
250
+ readonly skipped: number;
251
+ readonly failed: number;
252
+ }
253
+
254
+ export const seedTotals = (rows: readonly SeedPassRow[]): SeedTotals => ({
255
+ inserted: rows.reduce((sum, row) => sum + row.inserted, 0),
256
+ updated: rows.reduce((sum, row) => sum + row.updated, 0),
257
+ skipped: rows.reduce((sum, row) => sum + row.skipped, 0),
258
+ failed: rows.filter((row) => row.status === 'failed').length,
259
+ });
260
+
261
+ /**
262
+ * Slowest first, in both renderers: a seed run that got slow is diagnosed by which FILE took the
263
+ * time, and a list in run order buries that under whatever happens to be alphabetically first.
264
+ */
265
+ const slowestFirst = (rows: readonly SeedPassRow[]): readonly SeedPassRow[] =>
266
+ rows.toSorted((left, right) => right.ms - left.ms);
267
+
268
+ export const seedPassToJson = (rows: readonly SeedPassRow[]): JsonValue => ({
269
+ seeds: slowestFirst(rows).map((row) => ({
270
+ file: row.file,
271
+ name: row.name,
272
+ tier: row.tier,
273
+ status: row.status,
274
+ ms: row.ms,
275
+ inserted: row.inserted,
276
+ updated: row.updated,
277
+ skipped: row.skipped,
278
+ })),
279
+ totals: { ...seedTotals(rows) },
280
+ });
281
+
282
+ export const renderSeedTable = (rows: readonly SeedPassRow[]): readonly string[] =>
283
+ renderTable(
284
+ ['seed', 'tier', 'status', 'ms', 'inserted', 'updated', 'skipped'],
285
+ slowestFirst(rows).map((row) => [
286
+ row.name,
287
+ row.tier,
288
+ row.status,
289
+ String(row.ms),
290
+ String(row.inserted),
291
+ String(row.updated),
292
+ String(row.skipped),
293
+ ]),
294
+ );
package/src/dev-assets.ts CHANGED
@@ -14,7 +14,7 @@ import { applyCacheHeaders } from '@ultimat3/http';
14
14
  import type { IconPlan } from '@ultimat3/pwa';
15
15
  import { BuiltinImagePipeline, PwaIconMissingError, planIcons } from '@ultimat3/pwa';
16
16
  import type { ImageQuery, ImageTransformDriver } from '@ultimat3/seo';
17
- import { builtinImageDriver, parseImageQuery } from '@ultimat3/seo';
17
+ import { builtinImageDriver, DEFAULT_WIDTHS, parseImageQuery } from '@ultimat3/seo';
18
18
  import type { ImageFormat, ImageTransform, Storage } from '@ultimat3/storage';
19
19
  import { IMAGE_FORMATS, isTenantScoped, variantKey } from '@ultimat3/storage';
20
20
  import {
@@ -49,6 +49,24 @@ export const MEDIA_BASE_PATH = '/media';
49
49
  */
50
50
  const IMMUTABLE_IMAGE: CacheHint = { mode: 'immutable' };
51
51
 
52
+ /**
53
+ * Whether a variant is one the framework itself can MINT, and therefore one worth storing.
54
+ *
55
+ * The cache key is built entirely from caller-supplied query values, and a signed-in reader
56
+ * holding `storage:read` may ask for any of them on their own objects — so `?w=1`, `?w=2`, … each
57
+ * wrote a new object to the app's only disk. `@ultimat3/seo`'s `MAX_IMAGE_WIDTH` (8192) bounds the
58
+ * blast radius and does not close it: 8192 stored objects per source, per format, is amplification
59
+ * a tenant drives with a `for` loop.
60
+ *
61
+ * The set is `DEFAULT_WIDTHS` **plus the source's intrinsic width**, which is exactly what
62
+ * `usableWidths` puts in a `srcset` — clamping to the constant alone would refuse the widest entry
63
+ * of every image whose intrinsic width is not one of the eight, a URL the framework mints itself.
64
+ * Anything outside it is still SERVED: this decides what is written, not what is answered, so no
65
+ * caller gains a new 4xx and the disk stops growing on a stranger's key.
66
+ */
67
+ const isMintableWidth = (width: number | undefined, intrinsic: number): boolean =>
68
+ width === undefined || width === intrinsic || DEFAULT_WIDTHS.includes(width);
69
+
52
70
  const imageResponse = (bytes: Uint8Array, contentType: string, cache: CacheHint): Response =>
53
71
  applyCacheHeaders(
54
72
  // Copied, not passed through: a `Uint8Array<ArrayBufferLike>` may be backed by a
@@ -112,6 +130,7 @@ async function transformedVariant(
112
130
  }
113
131
 
114
132
  const source = await disk.get(key);
133
+ const intrinsic = probeImage(source.bytes).width;
115
134
  // The seam is WHICH driver transforms, not who resolves the bytes: `TransformRequest.width` is
116
135
  // required, and a request with no `?w=` gets its width from the source's own header — so the
117
136
  // read happens either way and a supplied driver is handed the same resolved request the builtin
@@ -122,11 +141,11 @@ async function transformedVariant(
122
141
  src: key,
123
142
  // A header read, not a decode: `?f=webp` alone still needs a width, and the source's own is
124
143
  // the only one that does not resize an image the caller never asked to resize.
125
- width: query.width ?? probeImage(source.bytes).width,
144
+ width: query.width ?? intrinsic,
126
145
  ...(query.format === undefined ? {} : { format: query.format }),
127
146
  ...(query.quality === undefined ? {} : { quality: query.quality }),
128
147
  });
129
- if (cached !== undefined) {
148
+ if (cached !== undefined && isMintableWidth(query.width, intrinsic)) {
130
149
  await disk.put(cached, variant.bytes, { contentType: variant.contentType });
131
150
  }
132
151
  return imageResponse(variant.bytes, variant.contentType, cache);
package/src/dev-cache.ts CHANGED
@@ -79,25 +79,25 @@ export function startCacheTiers(options: CacheTiersOptions): () => Promise<void>
79
79
  registerInvalidationBroadcast(async (wireTags) => {
80
80
  await options.transport.publish(CACHE_INVALIDATE_SUBJECT, JSON.stringify(wireTags));
81
81
  });
82
- let subscription: TransportSubscription | undefined;
83
- // Not awaited: the boot must not block on a subscribe, and a bus that refuses one is a process
84
- // that misses peer invalidations, never a process that fails to start.
85
- void options.transport
82
+ // Not awaited HERE: the boot must not block on a subscribe, and a bus that refuses one is a
83
+ // process that misses peer invalidations, never a process that fails to start. The PROMISE is
84
+ // held rather than a handle assigned inside a `.then`, because the release ran first whenever
85
+ // `stop()` beat the round trip — a NATS bus plus a boot that throws in `bootRoles`, or a test
86
+ // that boots and stops immediately — and the subscription that landed afterwards was live with
87
+ // nobody left holding it. `mcp-host.ts`'s lazy `started` is the same shape.
88
+ const subscribing: Promise<TransportSubscription | undefined> = options.transport
86
89
  .subscribe(CACHE_INVALIDATE_SUBJECT, (payload: string) => {
87
90
  void applyBroadcast(payload);
88
91
  })
89
- .then((handle) => {
90
- subscription = handle;
91
- })
92
92
  .catch((error: unknown) => {
93
93
  logger.warn('cache.broadcast.subscribe-failed', { error: messageOf(error) });
94
+ return undefined;
94
95
  });
95
96
 
96
97
  // `resetTiers()` drops the registry AND the broadcast in one call: this boot is the only thing
97
98
  // that registers either, and a tier left behind would purge for a process that has stopped.
98
99
  return async () => {
99
- subscription?.unsubscribe();
100
- subscription = undefined;
100
+ (await subscribing)?.unsubscribe();
101
101
  resetTiers();
102
102
  };
103
103
  }
package/src/dev-render.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  createIsrController,
25
25
  headFromMeta,
26
26
  hydrateRuntime,
27
+ isrKey,
27
28
  metaContextFor,
28
29
  renderComponent,
29
30
  renderHead,
@@ -185,7 +186,11 @@ async function resultFor(
185
186
  return { status: 200, headers: staticHeaders(contentHash(body), options.buildId), body };
186
187
  }
187
188
  case 'isr': {
188
- const served = await isr.serve(url.pathname, () =>
189
+ // `isrKey(url)`, never `url.pathname`: the query is part of what was rendered — this
190
+ // route's own `meta` reads `data.url` — so two URLs differing only in their query are two
191
+ // documents. Keyed on the pathname alone, the first render answered every later query
192
+ // string (#171). Render owns the derivation so no second caller can invent another.
193
+ const served = await isr.serve(isrKey(url), () =>
189
194
  documentFrom(entry, request, data, options),
190
195
  );
191
196
  return served.result;
package/src/dev-roles.ts CHANGED
@@ -294,9 +294,11 @@ export async function startRoles(options: StartRolesOptions): Promise<RunningRol
294
294
  // — every enqueue in a request handler silently becomes a job that never runs.
295
295
  //
296
296
  // On `worker`, and on `worker` alone: it is the role that exists wherever jobs run at all, and
297
- // a relay is safe to duplicate (publish-then-mark is at-least-once and the idempotency key
298
- // collapses the repeat) but pointless to spread. A deployment with no `worker` has no one to
299
- // run the jobs either way.
297
+ // a relay is safe to duplicate — the claim is a LEASE taken in the statement that locks the
298
+ // row, so two relays never hold one batch — but pointless to spread. The idempotency key is
299
+ // not the reason and never was: its conflict target is a partial index over live states, so it
300
+ // collapses a repeat only while the first job is still live. A deployment with no `worker` has
301
+ // no one to run the jobs either way.
300
302
  const relay: OutboxRelay | null = selected.includes('worker')
301
303
  ? createOutboxRelay({ store: options.runtime.outbox, driver: options.runtime.jobs })
302
304
  : null;
@@ -6,7 +6,7 @@
6
6
  import { mkdirSync } from 'node:fs';
7
7
  import type { PurgeDriver } from '@ultimat3/cache';
8
8
  import { isNoopPurgeDriver, selectPurgeDriver } from '@ultimat3/cache';
9
- import { isLocal, resolveEnvironment } from '@ultimat3/core';
9
+ import { isLocal, renderThrowable, resolveEnvironment } from '@ultimat3/core';
10
10
  import type { EventBus, JobDriver, OutboxStore } from '@ultimat3/jobs';
11
11
  import type { MailDriver } from '@ultimat3/mail';
12
12
  import {
@@ -167,7 +167,7 @@ export function startStorage(services: DevServices, env: Env, override?: Storage
167
167
  try {
168
168
  mkdirSync(root, { recursive: true });
169
169
  } catch (cause) {
170
- const detail = cause instanceof Error ? cause.message : String(cause);
170
+ const detail = renderThrowable(cause);
171
171
  throw new StorageUnwritableError(
172
172
  `the embedded storage disk needs ${root} and it could not be created: ${detail}`,
173
173
  `mount a writable volume at ${root}, or set S3_ENDPOINT and S3_BUCKET to use object storage instead`,
@@ -2,6 +2,10 @@
2
2
  // owns keys, bytes and the tenant boundary and owns no `Response`; `@ultimat3/policy` owns the
3
3
  // one authz decision; this file is where those two meet a `Route` — the same shape `dev-assets.ts`
4
4
  // uses for `/icons` and `/media`, so `x dev` and `apps/web/server.ts` mount one read path, not two.
5
+ //
6
+ // The base path is `@ultimat3/storage`'s `DEFAULT_SIGNED_URL_BASE`, imported and never restated:
7
+ // `localDriver` SIGNS `/_storage/<disk>/<key>`, so a local `'/_storage'` here is a second statement
8
+ // of one constant — and a signer and a reader that disagree serve 404 for every signed URL.
5
9
 
6
10
  import { actorOf } from '@ultimat3/action';
7
11
  import type { Actor } from '@ultimat3/core';
@@ -12,15 +16,13 @@ import { can, codeOf, evaluate, forbidden, reasonOf } from '@ultimat3/policy';
12
16
  import type { Storage, StorageRead } from '@ultimat3/storage';
13
17
  import {
14
18
  assertSafeKey,
19
+ DEFAULT_SIGNED_URL_BASE,
15
20
  isTenantScoped,
16
21
  isWithinOrg,
17
22
  objectNotFound,
18
23
  orgMismatch,
19
24
  } from '@ultimat3/storage';
20
25
 
21
- /** `localDriver` signs `/_storage/<disk>/<key>`, so the read half hangs off the same base. */
22
- export const STORAGE_BASE_PATH = '/_storage';
23
-
24
26
  /**
25
27
  * The one capability that gates reading a stored object, on every disk. A permission and not a
26
28
  * per-disk family: `disk` is in the policy's `input`, so an app that wants a per-disk rule writes
@@ -223,7 +225,7 @@ export function storageRoutes(options: StorageRoutesOptions): readonly Route[] {
223
225
  return [
224
226
  {
225
227
  method: 'GET',
226
- path: `${STORAGE_BASE_PATH}/:disk/*key`,
228
+ path: `${DEFAULT_SIGNED_URL_BASE}/:disk/*key`,
227
229
  meta: {
228
230
  name: 'storage.read',
229
231
  auth: 'required',
package/src/dev-traces.ts CHANGED
@@ -71,9 +71,27 @@ function requestFacts(root: ReadableSpan): { method: string; path: string } {
71
71
  };
72
72
  }
73
73
 
74
+ /** `http.request_id` is stamped by the pipeline's root and by nothing else; the name is a fallback. */
74
75
  const isHttpRoot = (span: ReadableSpan): boolean =>
75
- span.parentSpanId === undefined &&
76
- (span.attributes['http.request_id'] !== undefined || /^[A-Z]+ \//.test(span.name));
76
+ span.attributes['http.request_id'] !== undefined || /^[A-Z]+ \//.test(span.name);
77
+
78
+ /**
79
+ * The request's own span among a trace's. `parentSpanId === undefined` was a third CONDITION
80
+ * until `As of 2026-08`, and it dropped every request that arrived with an inbound
81
+ * `traceparent`: `pipeline.ts` passes `parent: correlation.parent`, so the root has a defined
82
+ * `parentSpanId`, `spans.find(isHttpRoot)` answered `undefined`, and the whole trace vanished
83
+ * from `/_x/timeline` for any caller behind an instrumented client, an ingress or a service
84
+ * mesh. It survives as the TIE-BREAK: the outermost candidate is the one whose parent is not
85
+ * itself in this recording, so a nested candidate can never outrank the request's own span.
86
+ */
87
+ function httpRootOf(spans: readonly ReadableSpan[]): ReadableSpan | undefined {
88
+ const candidates = spans.filter(isHttpRoot);
89
+ const recorded = new Set(spans.map((span) => span.context.spanId));
90
+ const outermost = candidates.find(
91
+ (span) => span.parentSpanId === undefined || !recorded.has(span.parentSpanId),
92
+ );
93
+ return outermost ?? candidates[0];
94
+ }
77
95
 
78
96
  function toTrace(root: ReadableSpan, spans: readonly ReadableSpan[]): RequestTrace {
79
97
  const { method, path } = requestFacts(root);
@@ -121,7 +139,11 @@ export function createTraceRecorder(options: { limit?: number } = {}): TraceReco
121
139
  const spans = byTrace.get(traceId);
122
140
  if (spans === undefined) {
123
141
  byTrace.set(traceId, [span]);
124
- // Bounded by trace, not by span: dropping half a request would leave a flame with holes.
142
+ // Bounded by TRACE, not by span: dropping half a request would leave a flame with holes.
143
+ // The cost is stated rather than capped — one trace's span array has no bound of its own, so
144
+ // a request issuing 50k statements holds 50k `ReadableSpan`s until it is evicted. That is a
145
+ // dev-only recorder (`serve.ts` installs none), and a per-trace cap would silently produce
146
+ // the holed flame this bound exists to prevent.
125
147
  while (byTrace.size > limit) {
126
148
  const oldest = byTrace.keys().next();
127
149
  if (oldest.done === true) break;
@@ -137,7 +159,7 @@ export function createTraceRecorder(options: { limit?: number } = {}): TraceReco
137
159
  traces(): readonly RequestTrace[] {
138
160
  const traces: RequestTrace[] = [];
139
161
  for (const spans of byTrace.values()) {
140
- const root = spans.find(isHttpRoot);
162
+ const root = httpRootOf(spans);
141
163
  if (root !== undefined) traces.push(toTrace(root, spans));
142
164
  }
143
165
  return traces.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
package/src/dispatch.ts CHANGED
@@ -3,10 +3,11 @@
3
3
  // path as results, so a failure is machine-readable exactly like a success.
4
4
 
5
5
  import { isAbsolute, resolve } from 'node:path';
6
- import { requireBunVersion } from './app-root';
6
+ import { requireAppRoot, requireBunVersion } from './app-root';
7
7
  import { createHelpCommand } from './cmd-help';
8
+ import { plannedCommandFor } from './cmd-planned';
8
9
  import type { CommandContext } from './command';
9
- import { UnknownCommandError } from './errors';
10
+ import { CliNotImplementedError, UnknownCommandError } from './errors';
10
11
  import type { Runner } from './exec';
11
12
  import { exec } from './exec';
12
13
  import type { CommandResult } from './output';
@@ -42,14 +43,33 @@ const errorResult = (command: string, error: unknown): CommandResult => ({
42
43
  * end without terminating the test runner.
43
44
  */
44
45
  export async function dispatch(options: DispatchOptions): Promise<number> {
45
- let args: ParsedArgs;
46
+ // Its own branch, ahead of the parse: an unsupported Bun is a fact about the environment and
47
+ // outranks anything argv says, including the planned pre-empt below.
46
48
  try {
47
49
  requireBunVersion(options.bunVersion);
50
+ } catch (error) {
51
+ options.write(render(errorResult('x', error), wantsJson(options.argv)));
52
+ return 1;
53
+ }
54
+
55
+ let args: ParsedArgs;
56
+ try {
48
57
  args = parseArgs(options.argv, SPECS);
49
58
  } catch (error) {
59
+ // A PLANNED command is not built, so every invocation of one must say that and nothing else.
60
+ // The parser refuses an undeclared flag before any `run` is reached and a planned command
61
+ // declares only the four globals, so `x logs tail --follow` reported X_CLI_BAD_FLAG — a flag
62
+ // list for a command that does not exist yet — while `x logs tail` reported the honest
63
+ // X_NOT_IMPLEMENTED with a runnable fix. Substituted here rather than in `parse.ts`, which is
64
+ // pure and knows nothing about what a command means; the precedent is the help swap below.
65
+ const planned = plannedCommandFor(commandFor(options.argv[0] ?? '')?.spec.name);
66
+ const failure =
67
+ planned === undefined
68
+ ? error
69
+ : new CliNotImplementedError({ feature: `x ${planned.name}`, fix: planned.fix });
50
70
  // `wantsJson`, not `includes('--json')`: a typo'd flag or a typo'd command is exactly the case
51
71
  // an agent hits while always passing `-j`, and the short form rendered prose it then parsed.
52
- const result = errorResult('x', error);
72
+ const result = errorResult(planned?.name ?? 'x', failure);
53
73
  options.write(render(result, wantsJson(options.argv)));
54
74
  return 1;
55
75
  }
@@ -85,6 +105,15 @@ export async function dispatch(options: DispatchOptions): Promise<number> {
85
105
  };
86
106
 
87
107
  try {
108
+ // The reader `CommandSpec.requiresApp` never had. Its doc said "the dispatcher enforces it" and
109
+ // `dispatch` did not read the field at all: the guarantee held only because all 17 declaring
110
+ // commands happen to call `requireAppRoot` themselves, so a new command that declares it and
111
+ // forgets the call ran outside an app with no refusal. Each of those 17 calls stays — they are
112
+ // what hands a command the root it works in, and several name a subcommand this cannot see
113
+ // (`env init`, `secrets set`) — but the DECLARATION is now what decides, ahead of any check a
114
+ // command makes about its own arguments. `--help` is exempt because `target` is then the help
115
+ // command, which declares nothing: usage for a command must be readable from anywhere.
116
+ if (target.spec.requiresApp === true) requireAppRoot(target.spec.name, ctx.cwd);
88
117
  const result = await target.run(ctx);
89
118
  options.write(render(result, args.json, args.flags.get('verbose') === true));
90
119
  // `x dev` and `x mcp serve --transport http` are still listening here: report first, so the
package/src/drift.ts CHANGED
@@ -62,6 +62,41 @@ export async function writeSchemaHash(root: string, migrationId: string): Promis
62
62
  return hash;
63
63
  }
64
64
 
65
+ /**
66
+ * "Some migration recorded this hash" — the one predicate, read by the check below AND by the
67
+ * reconcile above it. Two spellings would let `x db gen` report the sidecar written while
68
+ * `x verify` still reports drift over the same two files.
69
+ */
70
+ const isRecorded = (records: readonly MigrationRecord[], hash: string): boolean =>
71
+ records.some((record) => record.hash === hash);
72
+
73
+ export interface HashReconciliation {
74
+ readonly hash: string;
75
+ /** False when a sidecar already held this hash — nothing was written, and nothing needed to be. */
76
+ readonly written: boolean;
77
+ }
78
+
79
+ /**
80
+ * Re-record the sidecar for a migration that is already the right one. `SCHEMA_GLOB` covers every
81
+ * non-test file under `packages/db/src`, not only the ones that imply DDL, so editing a seed or a
82
+ * helper moves the hash with no diff behind it — and `X_DB_DRIFT`'s `fix:` has to have somewhere to
83
+ * land or the instruction is unfollowable. The caller owes the proof that the DDL genuinely did not
84
+ * move (`db-generate.ts` reaches this only on an empty diff off a fully loaded registry); this
85
+ * function decides only whether a write is needed.
86
+ *
87
+ * A hash an OLDER migration recorded is left alone: `checkSourceDrift` already answers clean on it,
88
+ * and stamping the newest sidecar would claim that migration produced a schema it did not.
89
+ */
90
+ export async function reconcileSchemaHash(
91
+ root: string,
92
+ migrationId: string,
93
+ ): Promise<HashReconciliation> {
94
+ const hash = await schemaHash(root);
95
+ if (isRecorded(await recordedHashes(root), hash)) return { hash, written: false };
96
+ await Bun.write(join(root, MIGRATIONS_DIR, hashFileName(migrationId)), `${hash}\n`);
97
+ return { hash, written: true };
98
+ }
99
+
65
100
  /**
66
101
  * How many entities the app declares. Injected so this module's own tests need no app on disk, and
67
102
  * so a caller that has already loaded the app can answer without loading it twice.
@@ -90,6 +125,11 @@ export async function checkSourceDrift(
90
125
  // `x db gen "initial"`, which has an empty diff there, writes no `.hash`, and exits ok: a fix
91
126
  // that succeeds and changes nothing. Drift resumes the moment the author declares an entity,
92
127
  // and by then the fix genuinely writes one.
128
+ //
129
+ // An empty diff still writes no `.hash` HERE, and must: `reconcileSchemaHash` needs a migration
130
+ // id to record against and there is no migration at all in this branch. With an entity declared
131
+ // the diff is never empty — a registry against zero migrations is `create table` for all of
132
+ // it — so this branch's fix stays the real generation it always was.
93
133
  if ((await declaredEntities()) === 0) return [];
94
134
  return [
95
135
  {
@@ -101,7 +141,7 @@ export async function checkSourceDrift(
101
141
  },
102
142
  ];
103
143
  }
104
- if (records.some((record) => record.hash === current)) return [];
144
+ if (isRecorded(records, current)) return [];
105
145
  return [
106
146
  {
107
147
  code: 'X_DB_DRIFT',