@ultimat3/cli 1.2.0 → 2.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 (138) hide show
  1. package/CLAUDE.md +724 -0
  2. package/README.md +41 -9
  3. package/package.json +25 -23
  4. package/src/api-routes.ts +16 -0
  5. package/src/app-auth.ts +32 -0
  6. package/src/app-entities.ts +18 -0
  7. package/src/app-env.ts +103 -0
  8. package/src/app-load.ts +20 -3
  9. package/src/bin.ts +4 -3
  10. package/src/budgets.ts +114 -9
  11. package/src/cmd-build.ts +69 -21
  12. package/src/cmd-db-branch.ts +215 -0
  13. package/src/cmd-db.ts +332 -155
  14. package/src/cmd-deploy.ts +59 -6
  15. package/src/cmd-dev.ts +83 -16
  16. package/src/cmd-docs.ts +167 -0
  17. package/src/cmd-doctor.ts +64 -9
  18. package/src/cmd-env.ts +95 -0
  19. package/src/cmd-errors.ts +33 -13
  20. package/src/cmd-fix.ts +5 -1
  21. package/src/cmd-generate.ts +146 -111
  22. package/src/cmd-help.ts +16 -5
  23. package/src/cmd-i18n.ts +2 -0
  24. package/src/cmd-jobs.ts +47 -33
  25. package/src/cmd-mcp.ts +11 -2
  26. package/src/cmd-new.ts +13 -7
  27. package/src/cmd-planned.ts +55 -10
  28. package/src/cmd-policy.ts +1 -0
  29. package/src/cmd-registries.ts +3 -0
  30. package/src/cmd-secrets.ts +368 -0
  31. package/src/cmd-tasks.ts +1 -0
  32. package/src/cmd-test.ts +17 -23
  33. package/src/cmd-verify.ts +177 -23
  34. package/src/db-backfill.ts +401 -0
  35. package/src/db-branch.ts +251 -0
  36. package/src/db-destructive.ts +29 -0
  37. package/src/db-finding.ts +28 -0
  38. package/src/db-generate.ts +112 -0
  39. package/src/db-snapshot.ts +24 -0
  40. package/src/dev-assets.ts +86 -20
  41. package/src/dev-cache.ts +122 -0
  42. package/src/dev-dashboard.ts +19 -4
  43. package/src/dev-hooks.ts +27 -2
  44. package/src/dev-n-plus-one.ts +191 -0
  45. package/src/dev-queue.ts +105 -19
  46. package/src/dev-render.ts +158 -26
  47. package/src/dev-roles-fixture.ts +67 -0
  48. package/src/dev-roles.ts +165 -78
  49. package/src/dev-runtime.ts +117 -40
  50. package/src/dev-services.ts +15 -0
  51. package/src/dev-storage.ts +245 -0
  52. package/src/dev-sync.ts +107 -0
  53. package/src/dev-traces.ts +11 -3
  54. package/src/dispatch.ts +4 -2
  55. package/src/document-styles.ts +54 -0
  56. package/src/drift.ts +37 -9
  57. package/src/error-catalog.ts +7 -18
  58. package/src/error-codes.ts +186 -0
  59. package/src/error-contract.ts +29 -7
  60. package/src/error-fixes.ts +114 -0
  61. package/src/errors.ts +201 -138
  62. package/src/fix-command.ts +268 -0
  63. package/src/flag-number.ts +56 -0
  64. package/src/framework-scope.ts +49 -0
  65. package/src/generate-kinds.ts +97 -0
  66. package/src/guards.ts +186 -0
  67. package/src/index.ts +84 -14
  68. package/src/island-bundle.ts +166 -0
  69. package/src/island-routes.ts +50 -0
  70. package/src/jobs-driver.ts +33 -0
  71. package/src/jobs-json.ts +24 -0
  72. package/src/jobs-report.ts +17 -4
  73. package/src/mcp-db-target.ts +52 -27
  74. package/src/mcp-errors.ts +120 -19
  75. package/src/mcp-host.ts +44 -25
  76. package/src/messages.ts +81 -2
  77. package/src/metrics-endpoint.ts +4 -3
  78. package/src/migrations.ts +37 -4
  79. package/src/otlp-export.ts +64 -0
  80. package/src/output.ts +46 -16
  81. package/src/parse.ts +41 -3
  82. package/src/policy-facts.ts +38 -6
  83. package/src/policy-fixture.ts +14 -7
  84. package/src/prerender.ts +111 -2
  85. package/src/registry.ts +21 -3
  86. package/src/runtime-overrides.ts +66 -0
  87. package/src/safe-url-label.ts +24 -0
  88. package/src/scaffold-fixture.ts +10 -0
  89. package/src/scaffold-typecheck.ts +16 -38
  90. package/src/serve.ts +170 -10
  91. package/src/source-files.ts +4 -0
  92. package/src/statement-loop.ts +74 -0
  93. package/src/style-csp.ts +18 -0
  94. package/src/sync-authenticator.ts +59 -0
  95. package/src/templates/action.ts +15 -30
  96. package/src/templates/admin-page.ts +103 -0
  97. package/src/templates/admin.ts +11 -7
  98. package/src/templates/backfill.ts +212 -0
  99. package/src/templates/entity.ts +72 -31
  100. package/src/templates/guard.ts +143 -0
  101. package/src/templates/index.ts +12 -1
  102. package/src/templates/island.ts +67 -0
  103. package/src/templates/job.ts +53 -13
  104. package/src/templates/naming.ts +17 -1
  105. package/src/templates/policy.ts +35 -28
  106. package/src/templates/query.ts +24 -5
  107. package/src/templates/resource.ts +19 -11
  108. package/src/templates/route.ts +90 -15
  109. package/src/templates/scaffold-app.ts +142 -45
  110. package/src/templates/scaffold-claude-agents.ts +149 -0
  111. package/src/templates/scaffold-claude-commands.ts +221 -0
  112. package/src/templates/scaffold-claude.ts +134 -0
  113. package/src/templates/scaffold-container.ts +46 -2
  114. package/src/templates/scaffold-db-package.ts +91 -0
  115. package/src/templates/scaffold-docs.ts +24 -5
  116. package/src/templates/scaffold-domain-package.ts +90 -0
  117. package/src/templates/scaffold-env.ts +87 -0
  118. package/src/templates/scaffold-i18n.ts +4 -1
  119. package/src/templates/scaffold-mcp-package.ts +49 -0
  120. package/src/templates/scaffold-package-shape.ts +25 -4
  121. package/src/templates/scaffold-repo.ts +116 -257
  122. package/src/templates/scaffold-roles.ts +68 -0
  123. package/src/templates/scaffold-ui-package.ts +56 -0
  124. package/src/templates/slice-foundation.ts +88 -0
  125. package/src/templates/wrap.ts +95 -0
  126. package/src/test-counts.ts +35 -0
  127. package/src/test-select.ts +30 -15
  128. package/src/test-shards.ts +21 -3
  129. package/src/test-workers.ts +47 -0
  130. package/src/ts-scan.ts +271 -13
  131. package/src/tsconfig-references.ts +78 -0
  132. package/src/verify-floor.ts +133 -0
  133. package/src/verify-step.ts +19 -0
  134. package/src/verify-test-run.ts +72 -0
  135. package/src/verify-tests.ts +160 -71
  136. package/src/version-loader.ts +20 -3
  137. package/src/workspace-checks.ts +87 -16
  138. package/src/write-line.ts +34 -0
@@ -66,50 +66,28 @@ export interface KnownGap {
66
66
  readonly owner: string;
67
67
  }
68
68
 
69
- // `InvariantColumns` is an index-signature type, so `c.title` is `ColumnExpr | undefined` under
70
- // `noUncheckedIndexedAccess`. Every hand-written entity in `examples/dummy` reproduces it
71
- // identically: the fix is a column proxy typed from the entity's own columns, in @ultimat3/entity
72
- // — a different template cannot avoid it without dropping to `satisfies()`, which would silently
73
- // stop emitting the Postgres CHECK.
74
- //
75
- // Measured, not guessed: no open-keyed form avoids the `| undefined` (index signature, `Record`,
76
- // and a mapped type over `string` or a template-literal pattern all produce it), and typing the
77
- // proxy from `columns` only reaches `c` when the element of `invariants:` is itself
78
- // context-sensitive. `invariant(name, build)` is a *call*, which TypeScript checks before
79
- // `entity()`'s own `C` is fixed, so `K` falls back to `string` and nothing changes. Making it
80
- // reach means changing the shape of `invariants:` — a documented primitive, so a major.
81
- const INVARIANT_PROXY =
82
- '@ultimat3/entity — type the invariant column proxy from the declared columns (needs a major: ' +
83
- 'the `invariants:` element shape has to become context-sensitive)';
84
-
85
- /** Every entity the fixture generates. Each one declares the same two invariants. */
86
- const FIXTURE_ENTITIES = [
87
- 'apps/web/app/credit-note/entity.ts',
88
- 'apps/web/app/invoice/entity.ts',
89
- 'apps/web/app/post/entity.ts',
90
- ] as const;
91
-
92
69
  /**
93
70
  * Diagnostics a template cannot fix, pinned one occurrence at a time. Pinned, never ignored:
94
71
  * `unexpectedIn` fails on anything not listed here — including a second copy of a listed
95
72
  * diagnostic, because each entry is consumed by exactly one match — and `staleGapsIn` fails when
96
73
  * a listed entry stops reproducing, so an entry cannot outlive the bug it describes.
74
+ *
75
+ * Empty, and that is the point: `invariants:` is one callback over the whole list, so `c` is typed
76
+ * from the entity's own columns and the six `TS18048 'c.title' is possibly 'undefined'` pins this
77
+ * list used to carry are gone. A new entry here is a template shipping a diagnostic, which needs
78
+ * an owner who can remove it — not a permanent exemption.
97
79
  */
98
- export const KNOWN_GAPS: readonly KnownGap[] = FIXTURE_ENTITIES.flatMap((file) =>
99
- ["'c.title' is possibly 'undefined'.", "'c.price' is possibly 'undefined'."].map(
100
- (message): KnownGap => ({
101
- variant: 'x new',
102
- code: 'TS18048',
103
- file,
104
- message,
105
- owner: INVARIANT_PROXY,
106
- }),
107
- ),
108
- );
109
-
110
- /** The pins one invocation may spend. Another variant's pins are not its to spend. */
111
- export const gapsFor = (variant: string): readonly KnownGap[] =>
112
- KNOWN_GAPS.filter((gap) => gap.variant === variant);
80
+ export const KNOWN_GAPS: readonly KnownGap[] = [];
81
+
82
+ /**
83
+ * The pins one invocation may spend. Another variant's pins are not its to spend. `gaps` is a
84
+ * parameter for the same reason it is on `unexpectedIn` and `staleGapsIn`: with `KNOWN_GAPS`
85
+ * legitimately empty, the bookkeeping is only testable against a list a test supplies.
86
+ */
87
+ export const gapsFor = (
88
+ variant: string,
89
+ gaps: readonly KnownGap[] = KNOWN_GAPS,
90
+ ): readonly KnownGap[] => gaps.filter((gap) => gap.variant === variant);
113
91
 
114
92
  const matches = (diagnostic: TypeDiagnostic, gap: KnownGap): boolean =>
115
93
  diagnostic.code === gap.code &&
package/src/serve.ts CHANGED
@@ -4,11 +4,25 @@
4
4
  // `dev: true`. The only production-shaped decisions live here: which role, which port, and the
5
5
  // fact that a container must bind every interface.
6
6
 
7
- import { listActions, toRoute } from '@ultimat3/action';
8
7
  import type { Role } from '@ultimat3/core';
9
- import { isRole, logger, ROLES } from '@ultimat3/core';
10
- import { type MigrationReport, migrate } from '@ultimat3/db';
8
+ import {
9
+ configureErrorReporting,
10
+ isRole,
11
+ logger,
12
+ ROLES,
13
+ sentryErrorReporter,
14
+ } from '@ultimat3/core';
15
+ import {
16
+ assertNoDrift,
17
+ checkDrift,
18
+ type DriftReport,
19
+ type MigrationReport,
20
+ migrate,
21
+ } from '@ultimat3/db';
11
22
  import type { Route } from '@ultimat3/http';
23
+ import { createIsrController } from '@ultimat3/render';
24
+ import { apiRoutes } from './api-routes';
25
+ import { loadSignInPath } from './app-auth';
12
26
  import { loadApp } from './app-load';
13
27
  import { appManifest } from './app-manifest';
14
28
  import { assetRoutes } from './dev-assets';
@@ -20,10 +34,15 @@ import type { RunningServices } from './dev-runtime';
20
34
  import { startServices } from './dev-runtime';
21
35
  import type { Env } from './dev-services';
22
36
  import { resolveServices } from './dev-services';
37
+ import { storageRoutes } from './dev-storage';
23
38
  import { PortInvalidError, RoleUnknownError } from './errors';
24
39
  import { holdUntilShutdown } from './hold';
40
+ import { buildIslands } from './island-bundle';
41
+ import { islandRoutes } from './island-routes';
25
42
  import { DEFAULT_METRICS_PORT } from './metrics-endpoint';
26
43
  import { readMigrations } from './migrations';
44
+ import { startOtlpExport } from './otlp-export';
45
+ import type { RuntimeOverrides } from './runtime-overrides';
27
46
 
28
47
  export const DEFAULT_PORT = 3000;
29
48
 
@@ -67,6 +86,31 @@ export function metricsPortFromEnv(env: Env): number {
67
86
  return portValue(env, 'METRICS_PORT', DEFAULT_METRICS_PORT);
68
87
  }
69
88
 
89
+ /**
90
+ * The one env var that turns error monitoring on, and the only vendor-shaped name in the boot
91
+ * path. Not a platform primitive (axiom 7): the value is a URL to whatever the operator runs, the
92
+ * wire format behind it is documented and self-hostable, and `SENTRY_DSN` is what every monitor
93
+ * that speaks it already documents — inventing a second spelling would mean an operator's existing
94
+ * tooling sets a variable this framework ignores. Exactly the precedent
95
+ * `OTEL_EXPORTER_OTLP_ENDPOINT` already sets in `docker/helm/values.yaml`.
96
+ */
97
+ export const ERROR_DSN_KEY = 'SENTRY_DSN';
98
+
99
+ /**
100
+ * Switch reporting on for this process. Unset DSN leaves core's no-op reporter in place, so a
101
+ * laptop and a CI run pay nothing and page nobody — and the release every event carries is the
102
+ * build id this boot already computed, never a second identity for the same deploy.
103
+ */
104
+ export function configureReporting(env: Env, buildId: string): void {
105
+ const dsn = env[ERROR_DSN_KEY]?.trim();
106
+ configureErrorReporting({
107
+ release: buildId,
108
+ // A malformed DSN throws here, at boot, rather than at the first outage: a monitor that was
109
+ // never connected looks exactly like an app that never failed.
110
+ ...(dsn === undefined || dsn.length === 0 ? {} : { reporter: sentryErrorReporter({ dsn }) }),
111
+ });
112
+ }
113
+
70
114
  export interface ServeOptions {
71
115
  readonly root: string;
72
116
  readonly env: Env;
@@ -76,6 +120,16 @@ export interface ServeOptions {
76
120
  readonly port?: number;
77
121
  /** Overrides `METRICS_PORT`, on the same terms. */
78
122
  readonly metricsPort?: number;
123
+ /**
124
+ * The drivers this deployment supplies instead of the ones the environment would select.
125
+ *
126
+ * This field is why `apps/web/server.ts` can stay three lines and still run a custom queue, a
127
+ * shared ISR store or an app's own middleware. Before it there was nowhere to hand the framework
128
+ * a driver, so the only way was an ambient setter from an app module — which `loadApp` imports
129
+ * AFTER `startServices` has captured its own, giving a process that enqueues to one queue and
130
+ * claims from another. `startRoles` now refuses that split outright.
131
+ */
132
+ readonly runtime?: RuntimeOverrides;
79
133
  }
80
134
 
81
135
  export interface ServedApp {
@@ -93,6 +147,8 @@ export interface MigratedApp {
93
147
  readonly kind: 'migrated';
94
148
  readonly role: 'migrate';
95
149
  readonly report: MigrationReport;
150
+ /** The post-condition: the live schema against the ledger this run just wrote. */
151
+ readonly drift: DriftReport;
96
152
  }
97
153
 
98
154
  export type StartedApp = ServedApp | MigratedApp;
@@ -106,9 +162,16 @@ export type StartedApp = ServedApp | MigratedApp;
106
162
  *
107
163
  * It boots the queue, not the whole runtime: this role touches the database and nothing else, and
108
164
  * `startQueue` is what installs `db()` for `migrate()` to find.
165
+ *
166
+ * The drift check is the post-condition, and it lives here rather than in `cmd-db.ts` for the same
167
+ * reason the migrator does: it needs the connection this function opened, and a developer and a
168
+ * release phase must not verify different things. It is **returned, never thrown** — the role's
169
+ * contract is "apply every migration, then exit", and a schema difference after a clean apply is a
170
+ * diagnostic, not a failed migration. `x db migrate` is where it is actionable, so `x db migrate`
171
+ * is what fails on it.
109
172
  */
110
173
  export async function runMigrations(options: ServeOptions): Promise<MigratedApp> {
111
- const queue = await startQueue(resolveServices(options.root, options.env));
174
+ const queue = await startQueue(resolveServices(options.root, options.env), options.runtime);
112
175
  try {
113
176
  const migrations = await readMigrations(options.root);
114
177
  const report = await migrate({
@@ -122,12 +185,43 @@ export async function runMigrations(options: ServeOptions): Promise<MigratedApp>
122
185
  available: migrations.length,
123
186
  appVersion: report.appVersion,
124
187
  });
125
- return { kind: 'migrated', role: 'migrate', report };
188
+ const drift = await checkDrift({ migrations });
189
+ // Logged with the first difference, not just a count: a release phase's log is the only place
190
+ // an operator sees this, and "3 differences" names nothing to act on.
191
+ if (!drift.ok) {
192
+ logger.warn('ultimate migrate drift', {
193
+ differences: drift.differences.length,
194
+ cause: drift.differences[0]?.cause,
195
+ fix: drift.differences[0]?.fix,
196
+ });
197
+ }
198
+ return { kind: 'migrated', role: 'migrate', report, drift };
126
199
  } finally {
127
200
  await queue.stop();
128
201
  }
129
202
  }
130
203
 
204
+ /**
205
+ * Release what a boot acquired before it failed, newest first.
206
+ *
207
+ * Every failure here is swallowed, because the step that refused to start is the one worth
208
+ * reporting — the same rule `startRoles`' own rollback runs by. Without it a throw between
209
+ * `startServices` and `startRoles` left the Postgres pool, the queue and the OTLP exporter running
210
+ * in a process whose caller has already given up: `x dev` and the container both retry the boot,
211
+ * and the second attempt met a `.x/pgdata` the first one still held.
212
+ */
213
+ export async function releaseBoot(
214
+ acquired: readonly (() => void | Promise<void>)[],
215
+ ): Promise<void> {
216
+ for (const release of [...acquired].reverse()) {
217
+ try {
218
+ await release();
219
+ } catch {
220
+ // Deliberately empty: see above.
221
+ }
222
+ }
223
+ }
224
+
131
225
  /**
132
226
  * Boot order is `x dev`'s, for the reason `x dev` gives: services, then the app's own modules
133
227
  * (importing them IS the registration), then the role that serves what they registered. The route
@@ -136,7 +230,29 @@ export async function runMigrations(options: ServeOptions): Promise<MigratedApp>
136
230
  */
137
231
  export async function serveApp(options: ServeOptions): Promise<ServedApp> {
138
232
  const role = options.role ?? roleFromEnv(options.env);
139
- const runtime = await startServices(resolveServices(options.root, options.env), options.env);
233
+ const runtime = await startServices(
234
+ resolveServices(options.root, options.env),
235
+ options.env,
236
+ options.runtime,
237
+ );
238
+ // Everything acquired from here down, in order, so a throw anywhere below gives it all back.
239
+ const acquired: (() => void | Promise<void>)[] = [() => runtime.stop()];
240
+ try {
241
+ return await bootRoles({ options, role, runtime, acquired });
242
+ } catch (error) {
243
+ await releaseBoot(acquired);
244
+ throw error;
245
+ }
246
+ }
247
+
248
+ /** The half of `serveApp` whose every acquisition is registered for rollback. */
249
+ async function bootRoles(boot: {
250
+ readonly options: ServeOptions;
251
+ readonly role: Role;
252
+ readonly runtime: RunningServices;
253
+ readonly acquired: (() => void | Promise<void>)[];
254
+ }): Promise<ServedApp> {
255
+ const { options, role, runtime, acquired } = boot;
140
256
  // Importing the app's modules IS the registration: every route, action and job below is
141
257
  // whatever this call put in the registries.
142
258
  await loadApp(options.root);
@@ -149,10 +265,38 @@ export async function serveApp(options: ServeOptions): Promise<ServedApp> {
149
265
  stamped !== undefined && stamped.length > 0
150
266
  ? stamped
151
267
  : (await appManifest(options.root)).manifest.buildId;
268
+ // Before the first socket opens: everything above this line fails loudly into the container's
269
+ // own logs, everything below it is a served request, a claimed job or a routed frame.
270
+ configureReporting(options.env, buildId);
271
+ // Beside error reporting, and for the same reason it is here: `OTEL_EXPORTER_OTLP_ENDPOINT` is
272
+ // in the shipped chart and nothing read it, so every deployment that configured a collector got
273
+ // an empty dashboard. `x dev` keeps its own recorder — the `/_x` timeline is a different sink
274
+ // with a different lifetime — so this is the production boot's alone (axiom 6).
275
+ const stopOtlp = startOtlpExport(options.env);
276
+ acquired.push(stopOtlp);
277
+ // Built at boot rather than shipped prebuilt, so the container serves the same chunks `x dev`
278
+ // does from the same source — the alternative is a second bundler invocation in the image build
279
+ // whose output nothing compares against the one the dev loop proved.
280
+ const islands = await buildIslands(options.root);
152
281
  const routes: readonly Route[] = [
153
- ...listActions().map(toRoute),
154
- ...assetRoutes({ root: options.root, storage: runtime.storage }),
155
- ...appRoutes({ buildId }),
282
+ ...apiRoutes(),
283
+ ...assetRoutes({
284
+ root: options.root,
285
+ storage: runtime.storage,
286
+ ...(options.runtime?.images === undefined ? {} : { images: options.runtime.images }),
287
+ }),
288
+ ...storageRoutes({ storage: runtime.storage }),
289
+ ...islandRoutes(() => islands),
290
+ ...appRoutes({
291
+ buildId,
292
+ resolveIsland: (file) => islands.resolverFor(file),
293
+ // Only when a store was supplied. `createIsrController` defaults to a per-process memory
294
+ // store, so twelve replicas hold twelve of them and a purge tag regenerates one twelfth of
295
+ // the fleet while the other eleven keep serving the page it just invalidated.
296
+ ...(options.runtime?.isrStore === undefined
297
+ ? {}
298
+ : { isr: createIsrController({ buildId, store: options.runtime.isrStore }) }),
299
+ }),
156
300
  ];
157
301
  const port = options.port ?? portFromEnv(options.env);
158
302
  // An in-process caller asking for an ephemeral app port is a test, and a test that grabbed the
@@ -169,8 +313,13 @@ export async function serveApp(options: ServeOptions): Promise<ServedApp> {
169
313
  runtime,
170
314
  routes,
171
315
  env: options.env,
316
+ // Same declaration `x dev` reads. Without it a container answers a browser that opened a
317
+ // guarded page with the problem document, rendered as raw JSON in the viewport.
318
+ signInPath: await loadSignInPath(options.root),
172
319
  http: CONTAINER_BINDING,
320
+ ...(options.runtime === undefined ? {} : { overrides: options.runtime }),
173
321
  });
322
+ acquired.push(() => running.stop());
174
323
  return {
175
324
  kind: 'served',
176
325
  role,
@@ -181,6 +330,9 @@ export async function serveApp(options: ServeOptions): Promise<ServedApp> {
181
330
  async stop() {
182
331
  await running.stop();
183
332
  await runtime.stop();
333
+ // Last: the exporters outlive the roles they were recording, so the drain's own spans and
334
+ // the final counter snapshot still have somewhere to go.
335
+ stopOtlp();
184
336
  },
185
337
  };
186
338
  }
@@ -192,7 +344,15 @@ export async function serveApp(options: ServeOptions): Promise<ServedApp> {
192
344
  */
193
345
  export async function runRole(options: ServeOptions): Promise<StartedApp> {
194
346
  const role = options.role ?? roleFromEnv(options.env);
195
- if (role === 'migrate') return runMigrations({ ...options, role });
347
+ if (role === 'migrate') {
348
+ const migrated = await runMigrations({ ...options, role });
349
+ // The release phase has one channel — the exit code — so drift is thrown here rather than
350
+ // returned. `x db migrate` calls the same `runMigrations` and renders every difference as a
351
+ // finding before exiting non-zero; a container that logged one and exited 0 would let the
352
+ // deploy roll on over a schema nobody can reconstruct, which is the failure drift exists for.
353
+ assertNoDrift(migrated.drift);
354
+ return migrated;
355
+ }
196
356
  const app = await serveApp({ ...options, role });
197
357
  logger.info('ultimate started', { role: app.role, url: app.url, buildId: app.buildId });
198
358
  await holdUntilShutdown('serve', () => app.stop())();
@@ -4,6 +4,10 @@
4
4
 
5
5
  export const SOURCE_GLOBS = [
6
6
  'packages/*/src/**/*.{ts,tsx}',
7
+ // Three packages carry an `e2e` directory beside `src`. It is shipped source by every rule that
8
+ // matters here — a 900-line file or an unrunnable `fix:` in one was invisible to `filesize` and
9
+ // `errors` alike, and `scripts/boundaries.ts` walked past it for the same reason.
10
+ 'packages/*/e2e/**/*.{ts,tsx}',
7
11
  'scripts/**/*.{ts,tsx}',
8
12
  'site/**/*.{ts,tsx}',
9
13
  'app/**/*.{ts,tsx}',
@@ -0,0 +1,74 @@
1
+ // One repeated statement shape, as every surface renders it. The ledger next door decides that a
2
+ // shape repeated; `@ultimat3/entity` decides which fix it has earned; this file is the one
3
+ // projection all four surfaces read — so `x dev`'s findings, the `/_x` timeline, the browser
4
+ // overlay and the log line can never disagree about a loop's code, its cause or the line that ends it.
5
+
6
+ import type { StatementLoopFact } from '@ultimat3/admin/dev';
7
+ import { logger } from '@ultimat3/core';
8
+ import { nPlusOne } from '@ultimat3/entity';
9
+ import type { OverlayNotice } from '@ultimat3/http';
10
+ import type { RepeatedStatement } from './dev-n-plus-one';
11
+ import type { Finding } from './output';
12
+
13
+ /**
14
+ * The verdict, as an error — built here rather than in the ledger because the count keeps rising
15
+ * after a shape is promoted: a loop of fifty reads `ran 50 times` when a surface asks, not
16
+ * `ran 5 times` because that is where the threshold sat. `nPlusOne` is `@ultimat3/entity`'s, and
17
+ * deliberately: the `fix:` names `preload`, `insertAll` and `updateWhere`, which are that package's
18
+ * vocabulary and derived from the relations the schema already declared — a line composed here
19
+ * would be a second answer to "what ends this loop", one the schema never agreed to.
20
+ */
21
+ export function loopFacts(repeat: RepeatedStatement): StatementLoopFact {
22
+ const attribution = repeat.attribution;
23
+ const error = nPlusOne({
24
+ kind: repeat.kind,
25
+ subject: repeat.fingerprint,
26
+ count: repeat.count,
27
+ entity: attribution?.entity,
28
+ op: attribution?.op,
29
+ });
30
+ return {
31
+ requestId: repeat.requestId,
32
+ code: error.code,
33
+ cause: error.cause,
34
+ fix: error.fix,
35
+ docs: error.docs ?? null,
36
+ subject: repeat.fingerprint,
37
+ count: repeat.count,
38
+ sample: repeat.sample,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * The `x dev` half. `at` is the request id and not a file: a loop has no line to open — it is a
44
+ * page's worth of statements — and the id is what joins this finding to the timeline's own row and
45
+ * to the log line the same request emitted.
46
+ */
47
+ export const loopFinding = (facts: StatementLoopFact): Finding => ({
48
+ code: facts.code,
49
+ cause: facts.cause,
50
+ fix: facts.fix,
51
+ at: facts.requestId,
52
+ ...(facts.docs === null ? {} : { docs: facts.docs }),
53
+ });
54
+
55
+ /** The browser half. `exactOptionalPropertyTypes`: an absent doc link is omitted, never `undefined`. */
56
+ export const loopNotice = (facts: StatementLoopFact): OverlayNotice => ({
57
+ code: facts.code,
58
+ cause: facts.cause,
59
+ fix: facts.fix,
60
+ ...(facts.docs === null ? {} : { docs: facts.docs }),
61
+ });
62
+
63
+ /**
64
+ * The log half, emitted once per request per code by the ledger that counts.
65
+ *
66
+ * The root logger and not `ctx.logger`, because this runs inside the request's own ALS scope and
67
+ * core's `setLoggerContextFields` puts `requestId` and `traceId` on every line emitted there — so
68
+ * the ids ride along without this file reaching for a context it would then have to prove it had.
69
+ * One line, in the 3-line contract's own order, because a warning an agent has to reassemble from
70
+ * three log records is a warning it acts on in three passes.
71
+ */
72
+ export const warnLoop = (facts: StatementLoopFact): void => {
73
+ logger.warn(`${facts.code}: ${facts.cause} — fix: ${facts.fix}`);
74
+ };
@@ -0,0 +1,18 @@
1
+ // Every inline `<style>` body a served process can put in a document, as the `style-src` sources
2
+ // that admit it. Read from the stylesheet registry at boot rather than checked in as a constant:
3
+ // importing the app's modules IS what fills that registry, so a committed hash would describe a
4
+ // stylesheet the document no longer carries — and the CSP would block the framework's own CSS.
5
+
6
+ import { cspHashSource } from '@ultimat3/http';
7
+ import { SURFACES, stylesFor } from '@ultimat3/render';
8
+
9
+ /**
10
+ * Call AFTER `loadApp`. One hash per distinct body: `stylesFor` is what `dev-render.ts` puts in
11
+ * the tag, per surface, so hashing the same call is the only way the two cannot drift. `extra`
12
+ * carries the documents this package does not render — `/_x`'s shell — because the caller is what
13
+ * knows which of them it mounted.
14
+ */
15
+ export function inlineStyleSources(extra: readonly string[] = []): readonly string[] {
16
+ const bodies = [...SURFACES.map((surface) => stylesFor(surface)), ...extra];
17
+ return [...new Set(bodies.filter((body) => body.length > 0).map(cspHashSource))].sort();
18
+ }
@@ -0,0 +1,59 @@
1
+ // The app's HTTP authenticator, seen as the `sync` node's. Until this file `sync-node.ts` was
2
+ // handed no `authenticate` by any host, so every socket the framework ever opened carried
3
+ // `actorId: null` — and the channel guard, the live-query gate, the presence entry and the
4
+ // per-tenant subscription cap all decided against an anonymous actor. Realtime was single-tenant
5
+ // by wiring, not by design.
6
+
7
+ import type { Actor } from '@ultimat3/core';
8
+ import type { HttpConfig } from '@ultimat3/http';
9
+ import {
10
+ configuredAuthenticator,
11
+ createRequestContext,
12
+ defineHttpConfig,
13
+ UltimateRequest,
14
+ } from '@ultimat3/http';
15
+ import type { SyncAuthenticator, SyncGrant } from '@ultimat3/realtime';
16
+
17
+ /**
18
+ * The upgrade request, dressed as the request an `Authenticator` reads.
19
+ *
20
+ * A websocket upgrade IS an HTTP request — same cookies, same `Authorization` header — so the
21
+ * app's one resolver answers it, and an app does not write a second identity for its sockets.
22
+ * The limiter is off because nothing in this config path serves a request: it exists so
23
+ * `ctx.config` is a real `HttpConfig`, and a rate limit resolved here would be a second, unread
24
+ * declaration of the app's own numbers.
25
+ */
26
+ function upgradeConfig(buildId: string): HttpConfig {
27
+ return defineHttpConfig({ buildId, rateLimit: { enabled: false, scope: 'process' } });
28
+ }
29
+
30
+ /**
31
+ * What the sync node is given when the app configured an authenticator, and `undefined` when it
32
+ * did not — which keeps `x dev` anonymous and makes the node log that it is, exactly as
33
+ * `createSyncNode` documents. A stub that answered `{ actor: anonymous }` would look configured.
34
+ *
35
+ * The grant carries **no `expiresAt` and no `refresh`**, and that is the honest limit of this
36
+ * adapter rather than an omission: `configureAuthenticator()` resolves an `Actor` and says nothing
37
+ * about how long it stays true, so inventing a window here would either close live sockets that
38
+ * are still authorized or claim a lifetime the app never promised. A deployment whose credential
39
+ * has a real expiry passes `runtime.syncAuthenticate` and gets re-authorization; the timer for it
40
+ * already lives in `createSyncNode.start()`.
41
+ */
42
+ export function syncAuthenticator(buildId: string): SyncAuthenticator | undefined {
43
+ const authenticate = configuredAuthenticator();
44
+ if (authenticate === undefined) return undefined;
45
+ // Once per node, not once per upgrade: resolving a config is pure and a 50k-socket node pays
46
+ // this per connection otherwise.
47
+ const config = upgradeConfig(buildId);
48
+ return async (request: Request): Promise<SyncGrant | null> => {
49
+ const ctx = createRequestContext({
50
+ url: new URL(request.url),
51
+ method: request.method,
52
+ role: 'sync',
53
+ config,
54
+ requestHeaders: request.headers,
55
+ });
56
+ const actor: Actor | null = await authenticate(new UltimateRequest(request, ctx), ctx);
57
+ return actor === null ? null : { actor };
58
+ };
59
+ }
@@ -5,6 +5,8 @@
5
5
  import type { FeatureTarget } from './entity';
6
6
  import type { GeneratedFile, NameSet } from './naming';
7
7
  import { names } from './naming';
8
+ import { sliceFoundation } from './slice-foundation';
9
+ import { wrapImport } from './wrap';
8
10
 
9
11
  const actionSource = (
10
12
  name: NameSet,
@@ -18,7 +20,7 @@ import { action, t } from '@ultimat3/action';
18
20
  // slice's own files and are shared by every action in it.
19
21
 
20
22
  import { ${feature.pascal}NotFoundError } from '../errors';
21
- import { can${feature.pascal}Write, ${feature.camel}Tag } from '../policy';
23
+ ${wrapImport([`can${feature.pascal}Write`, `${feature.camel}Tag`], '../policy')}
22
24
  import * as repo from '../repo';
23
25
 
24
26
  export const ${name.camel} = action({
@@ -28,7 +30,7 @@ export const ${name.camel} = action({
28
30
  output: t.object({ id: t.uuid, title: t.string }),
29
31
  policy: can${feature.pascal}Write,
30
32
  cache: { invalidates: [${feature.camel}Tag] },
31
- mcp: { expose: true, description: '${name.raw} — generated, edit the description' },
33
+ mcp: { expose: true, description: '${name.raw} — edit this description' },
32
34
  async handle({ input }) {
33
35
  const row = await repo.byId(input.id);
34
36
  if (row === undefined) throw new ${feature.pascal}NotFoundError({ id: input.id });
@@ -58,7 +60,7 @@ export const ${name.camel} = mutator({
58
60
  input: t.object({ id: t.uuid, orgId: t.uuid, title: t.string }),
59
61
  output: t.object({ id: t.uuid, title: t.string }),
60
62
  policy: can${feature.pascal}Write,
61
- mcp: { expose: true, description: '${name.raw} — generated, edit the description' },
63
+ mcp: { expose: true, description: '${name.raw} — edit this description' },
62
64
  // tx.table(name) rather than tx.${feature.plural}: the typed accessor exists only once the app
63
65
  // augments LocalTables, and generated code cannot assume that has happened yet. The name is the
64
66
  // entity's snake_case table, so the local twin and the server row live under one key.
@@ -77,25 +79,6 @@ export const ${name.camel} = mutator({
77
79
  });
78
80
  `;
79
81
 
80
- const errorsSource = (
81
- feature: NameSet,
82
- ): string => `// The ${feature.kebab} feature's X_* codes. Never throw a bare Error: an agent reading the failure
83
- // needs the code, the cause and the exact command that fixes it.
84
-
85
- import { UltimateError } from '@ultimat3/core';
86
-
87
- export class ${feature.pascal}NotFoundError extends UltimateError {
88
- constructor(input: { id: string }) {
89
- super({
90
- code: 'X_${feature.kebab.toUpperCase().split('-').join('_')}_NOT_FOUND',
91
- cause: \`no ${feature.kebab} with id \${input.id}\`,
92
- fix: 'x db studio to confirm the row exists, or pass an id from the list query',
93
- docs: 'https://ultimate.dev/errors/X_NOT_FOUND',
94
- });
95
- }
96
- }
97
- `;
98
-
99
82
  const ID = '00000000-0000-4000-8000-000000000001';
100
83
  const ORG = '00000000-0000-4000-8000-000000000002';
101
84
  const OTHER_ORG = '00000000-0000-4000-8000-000000000009';
@@ -121,7 +104,9 @@ const actionTest = (
121
104
  name: NameSet,
122
105
  feature: NameSet,
123
106
  isMutator: boolean,
124
- ): string => `import { testActor } from '@ultimat3/policy';
107
+ ): string => `// ${name.camel}: its declared shape, the input it refuses, the contract every action owes, and the
108
+ // foreign-org actor it denies before the handler runs. One declaration, every surface.
109
+ import { testActor } from '@ultimat3/policy';
125
110
  import { contractTest, expect, unitTest } from '@ultimat3/testing';
126
111
  import { ${name.camel} } from './${name.kebab}';
127
112
 
@@ -148,21 +133,21 @@ unitTest('${name.camel} rejects input that is not a uuid', async () => {
148
133
  await expect(target.input).toAcceptInput(input);
149
134
  });
150
135
 
151
- contractTest('${name.camel} passes the contract every action owes', async () => {
136
+ contractTest('${name.camel} passes the action contract', async () => {
152
137
  // Three assertions the framework makes for any action, without knowing what this one does:
153
138
  // garbage input is rejected, an anonymous actor is denied, and the operation reaches the
154
139
  // OpenAPI document. \`.contract()\` is the projection; this loop just runs it.
155
140
  for (const contract of target.contract()) await contract.run();
156
141
  });
157
142
 
158
- contractTest('${name.camel} denies a foreign org before the handler runs', async () => {
143
+ contractTest('${name.camel} denies a foreign org', async () => {
159
144
  // \`.as()\` is the one execution path with the actor swapped, so this denial is the same one
160
145
  // HTTP, MCP and the job surface would produce — and no repo call happened to produce it.
161
146
  const denied = await target.as(outsider, input).catch((error: unknown) => error);
162
147
  expect(denied).toBeUltimateError('X_FORBIDDEN');
163
148
  });
164
149
 
165
- contractTest('${name.camel} projects one MCP tool and one OpenAPI operation', () => {
150
+ contractTest('${name.camel} projects one tool and one operation', () => {
166
151
  // Same policy object on both surfaces — an MCP call cannot reach a different authz path.
167
152
  expect(target.tool().policy).toBe(target.policy);
168
153
  expect(target.tool().description).not.toBe('');
@@ -180,14 +165,14 @@ export function actionFiles(rawName: string, target: ActionOptions): readonly Ge
180
165
  const dir = `${target.surfaceDir}/${target.feature}/actions`;
181
166
  const isMutator = target.mutator === true;
182
167
  return [
168
+ // The three slice modules this action's source imports — `../errors`, `../policy`, `../repo`
169
+ // (which comes with `../entity`, its row type). Composed rather than assumed: `x g action`
170
+ // into a slice no `x g resource` had created emitted all three imports and wrote none of them.
171
+ ...sliceFoundation(target, ['entity', 'policy', 'errors']),
183
172
  {
184
173
  path: `${dir}/${name.kebab}.ts`,
185
174
  contents: isMutator ? mutatorSource(name, feature) : actionSource(name, feature),
186
175
  },
187
176
  { path: `${dir}/${name.kebab}.test.ts`, contents: actionTest(name, feature, isMutator) },
188
- {
189
- path: `${target.surfaceDir}/${target.feature}/errors.ts`,
190
- contents: errorsSource(feature),
191
- },
192
177
  ];
193
178
  }