@ultimat3/cli 6.0.0 → 8.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 (91) hide show
  1. package/CLAUDE.md +65 -5
  2. package/README.md +8 -3
  3. package/package.json +25 -24
  4. package/src/affected.ts +320 -0
  5. package/src/app-boundaries.ts +55 -5
  6. package/src/bin.ts +6 -3
  7. package/src/browser-launcher.ts +109 -0
  8. package/src/ci-log.ts +0 -0
  9. package/src/ci-runs.ts +179 -0
  10. package/src/cmd-affected.ts +109 -0
  11. package/src/cmd-build.ts +29 -3
  12. package/src/cmd-ci.ts +273 -0
  13. package/src/cmd-db-backfill.ts +240 -0
  14. package/src/cmd-db-branch.ts +3 -2
  15. package/src/cmd-db.ts +35 -156
  16. package/src/cmd-deploy.ts +37 -3
  17. package/src/cmd-dev.ts +7 -1
  18. package/src/cmd-errors.ts +2 -3
  19. package/src/cmd-fix.ts +3 -3
  20. package/src/cmd-i18n.ts +67 -5
  21. package/src/cmd-jobs.ts +27 -4
  22. package/src/cmd-mcp.ts +18 -9
  23. package/src/cmd-new.ts +91 -4
  24. package/src/cmd-policy.ts +3 -2
  25. package/src/cmd-pr.ts +359 -0
  26. package/src/cmd-registries.ts +3 -2
  27. package/src/cmd-shot.ts +382 -0
  28. package/src/cmd-tasks.ts +9 -4
  29. package/src/cmd-test.ts +96 -7
  30. package/src/cmd-verify.ts +47 -6
  31. package/src/dev-cache.ts +1 -1
  32. package/src/dev-lock.ts +124 -12
  33. package/src/dev-queue.ts +12 -7
  34. package/src/dev-replicator.ts +3 -7
  35. package/src/dev-roles-fixture.ts +1 -1
  36. package/src/dev-roles.ts +40 -8
  37. package/src/dev-runtime.ts +96 -4
  38. package/src/dev-sync.ts +9 -4
  39. package/src/dispatch.ts +35 -5
  40. package/src/drift.ts +52 -7
  41. package/src/error-codes.ts +21 -0
  42. package/src/framework-scope.ts +57 -5
  43. package/src/generate-kinds.ts +19 -1
  44. package/src/gh-target.ts +118 -0
  45. package/src/gh.ts +204 -0
  46. package/src/i18n-registration.ts +67 -4
  47. package/src/index.ts +38 -1
  48. package/src/island-bundle.ts +62 -3
  49. package/src/island-solid-production.ts +129 -0
  50. package/src/island-styles.ts +41 -0
  51. package/src/jobs-report.ts +10 -13
  52. package/src/mcp-errors.ts +12 -0
  53. package/src/messages.ts +76 -0
  54. package/src/output.ts +22 -2
  55. package/src/parse.ts +81 -37
  56. package/src/pr-threads.ts +291 -0
  57. package/src/prerender.ts +52 -10
  58. package/src/realtime-browser-probe-fixture.ts +9 -0
  59. package/src/registry.ts +8 -0
  60. package/src/runtime-overrides.ts +11 -3
  61. package/src/shot-settle.ts +57 -0
  62. package/src/shot-verdict.ts +360 -0
  63. package/src/static-report.ts +219 -0
  64. package/src/sync-authenticator.ts +86 -14
  65. package/src/templates/guard-bare-error.ts +122 -0
  66. package/src/templates/guard-raw-colour.ts +138 -0
  67. package/src/templates/guard-untranslated-string.ts +138 -0
  68. package/src/templates/guard-unzoned-date.ts +142 -0
  69. package/src/templates/index.ts +4 -0
  70. package/src/templates/island-fixture.ts +76 -0
  71. package/src/templates/island.ts +130 -18
  72. package/src/templates/resource-form-island.ts +279 -0
  73. package/src/templates/resource.ts +20 -41
  74. package/src/templates/route.ts +15 -2
  75. package/src/templates/scaffold-app.ts +13 -78
  76. package/src/templates/scaffold-container.ts +30 -4
  77. package/src/templates/scaffold-db-package.ts +46 -7
  78. package/src/templates/scaffold-docs.ts +24 -13
  79. package/src/templates/scaffold-entries.ts +131 -0
  80. package/src/templates/scaffold-guards.ts +26 -0
  81. package/src/templates/scaffold-mcp-package.ts +35 -2
  82. package/src/templates/scaffold-package-shape.ts +7 -2
  83. package/src/templates/scaffold-repo.ts +37 -6
  84. package/src/test-select.ts +4 -3
  85. package/src/test-shards.ts +19 -3
  86. package/src/verify-checks.ts +11 -1
  87. package/src/verify-run.ts +25 -3
  88. package/src/verify-step.ts +11 -2
  89. package/src/verify-tests.ts +11 -3
  90. package/src/workspace-graph.ts +241 -0
  91. package/src/write-line.ts +23 -5
@@ -0,0 +1,279 @@
1
+ // The slice's client entry: `x g resource <name>` emits its form as an ISLAND, because that is the
2
+ // one shape the framework compiles for a browser. A plain `.tsx` with a signal and an `onSubmit`
3
+ // is not a smaller version of this — the island glob never discovers it, and a server render drops
4
+ // every `on*` prop (`packages/render/src/html.ts`) and reads each signal exactly once.
5
+
6
+ // Bun ships no path API, and the one arithmetic this file does is a specifier: the island's path
7
+ // seen from the page's directory, which `island-bundle.ts` resolves the same way at build time.
8
+ import { posix } from 'node:path';
9
+ import { upToAppRoot } from './island';
10
+ import type { GeneratedFile, NameSet } from './naming';
11
+
12
+ /**
13
+ * What the PAGE has to write, which is not what the island's own directory would suggest.
14
+ *
15
+ * `island({ src })` resolves against the route file, and `x g resource widget` writes its page at
16
+ * `apps/web/app/widgets/` while the island lands in `apps/web/app/widget/` — so the `'./…'` this
17
+ * file used to print resolved to `apps/web/app/widgets/widget-form.island.tsx`, a path the build
18
+ * never bundles, and an author following the comment got `X_ISLAND_INVALID`. Derived from the two
19
+ * directories rather than written down, so it cannot disagree with where the files actually go.
20
+ */
21
+ export const formIslandSpecifier = (feature: NameSet, dir: string, pageDir: string): string => {
22
+ const relative = posix.join(posix.relative(pageDir, dir), `${feature.kebab}-form.island.tsx`);
23
+ return relative.startsWith('.') ? relative : `./${relative}`;
24
+ };
25
+
26
+ const formIslandSource = (
27
+ feature: NameSet,
28
+ specifier: string,
29
+ ): string => `// ${feature.pascal}Form: the only module of the ${feature.kebab} slice a browser downloads.
30
+ //
31
+ // The page names this file by SPECIFIER, never by import — and the specifier is resolved against
32
+ // the PAGE, which is one directory across from this one:
33
+ // const ${feature.pascal}Form = island({
34
+ // src: '${specifier}',
35
+ // props: ['endpoint', 'locale', 'labels'],
36
+ // });
37
+ // <${feature.pascal}Form endpoint={derivePath('create${feature.pascal}').path} locale={locale} labels={labels} />
38
+ // A string has no import edge, so the page's bundle graph stays the page's (axiom 6).
39
+
40
+ import { Button, Form, Input, setSolidRuntime, UiProvider } from '@ultimat3/ui';
41
+ import type { JSX } from 'solid-js';
42
+ import * as solidRuntime from 'solid-js';
43
+ import { createSignal } from 'solid-js';
44
+ import { render } from 'solid-js/web';
45
+ import styles from './ui.module.scss';
46
+
47
+ /** Every string this module renders, and the path it posts to. An island's props cross the seam as
48
+ * JSON inside the document, so \`t()\`'s catalog cannot travel — the server translates. */
49
+ export interface ${feature.pascal}FormProps {
50
+ /** \`derivePath('create${feature.pascal}').path\`, minted on the server: one namer for the route. */
51
+ readonly endpoint: string;
52
+ /** The request's own locale. A browser has no ambient one the server ever agreed to. */
53
+ readonly locale: string;
54
+ readonly labels: {
55
+ readonly title: string;
56
+ readonly submit: string;
57
+ readonly saved: string;
58
+ readonly retry: string;
59
+ };
60
+ }
61
+
62
+ type SaveState = 'idle' | 'saved' | 'failed';
63
+
64
+ /**
65
+ * Presentation only: the action this submits to owns validation server-side, so the form never
66
+ * re-implements the invariant — a blank title fails at the boundary, not in the DOM.
67
+ *
68
+ * A plain \`fetch\` to the path the server minted, not the typed client: \`rpc()\` pulls
69
+ * \`@ultimat3/action\` into the chunk, which is larger than everything else here put together. The
70
+ * naming rule is still the framework's; only the transport is second.
71
+ */
72
+ function ${feature.pascal}FormBody(props: ${feature.pascal}FormProps): JSX.Element {
73
+ const [title, setTitle] = createSignal('');
74
+ const [state, setState] = createSignal<SaveState>('idle');
75
+
76
+ // A rejected \`fetch\` — offline, DNS, an aborted request — is the same OUTCOME as a refused one,
77
+ // and it is the one \`retry\` exists for. Without the catch, \`setState\` is never reached: the
78
+ // status line stays empty, and the rejection escapes the \`void send()\` below as an unhandled one.
79
+ const send = async (): Promise<void> => {
80
+ try {
81
+ const response = await fetch(props.endpoint, {
82
+ method: 'POST',
83
+ headers: { 'content-type': 'application/json' },
84
+ body: JSON.stringify({ title: title() }),
85
+ });
86
+ setState(response.ok ? 'saved' : 'failed');
87
+ } catch {
88
+ setState('failed');
89
+ }
90
+ };
91
+
92
+ const status = (): string => {
93
+ if (state() === 'saved') return props.labels.saved;
94
+ return state() === 'failed' ? props.labels.retry : '';
95
+ };
96
+
97
+ return (
98
+ <Form
99
+ class={styles.item}
100
+ onSubmit={(event) => {
101
+ event.preventDefault();
102
+ void send();
103
+ }}
104
+ >
105
+ <Input
106
+ aria-label={props.labels.title}
107
+ value={title()}
108
+ onInput={(event) => setTitle(event.currentTarget.value)}
109
+ />
110
+ <Button type="submit">{props.labels.submit}</Button>
111
+ <p data-role="status" role="status" aria-live="polite">
112
+ {status()}
113
+ </p>
114
+ </Form>
115
+ );
116
+ }
117
+
118
+ /**
119
+ * The one export the hydration runtime calls — \`import(entry).then((m) => m.mount(el, props))\`.
120
+ *
121
+ * \`setSolidRuntime\` comes FIRST and it is not optional: \`@ultimat3/ui\` imports *types* from
122
+ * solid-js and never a runtime, so the reactive graph a component reaches is the one an entry
123
+ * registers. Delete the line and the first \`<UiProvider>\` render throws X_UI_RUNTIME_MISSING —
124
+ * loud on purpose, because a DOM render that lost its runtime is a theme toggle that does nothing.
125
+ *
126
+ * The shell is cleared first: Solid's \`render\` APPENDS when the container already has children,
127
+ * so without it the server's markup stays on screen above a second, live copy of the same thing.
128
+ */
129
+ export function mount(el: HTMLElement, props: ${feature.pascal}FormProps): void {
130
+ setSolidRuntime(solidRuntime);
131
+ el.textContent = '';
132
+ render(
133
+ () => (
134
+ <UiProvider locale={props.locale}>
135
+ <${feature.pascal}FormBody {...props} />
136
+ </UiProvider>
137
+ ),
138
+ el,
139
+ );
140
+ }
141
+ `;
142
+
143
+ const formIslandTest = (
144
+ feature: NameSet,
145
+ dir: string,
146
+ ): string => `// The form the browser actually runs. \`mountIsland\` builds this entry with the same
147
+ // \`buildIslands\` that \`x build\` and \`x dev\` use, imports the emitted chunk the way the hydration
148
+ // runtime does, and drives \`mount\` against a DOM small enough to read.
149
+ //
150
+ // It is the test that keeps this file a CLIENT entry. A generated form that only typechecks is
151
+ // what shipped before: server-rendered, every \`on*\` prop dropped, every signal read once.
152
+
153
+ import { join } from 'node:path';
154
+ import { buildIslands } from '@ultimat3/cli';
155
+ import {
156
+ afterAll,
157
+ beforeAll,
158
+ describe,
159
+ expect,
160
+ type FakeElement,
161
+ type MountedIsland,
162
+ mountIsland,
163
+ test,
164
+ } from '@ultimat3/testing';
165
+
166
+ const APP_ROOT = join(import.meta.dir, ${upToAppRoot(dir)});
167
+ const ISLAND = '${dir}/${feature.kebab}-form.island.tsx';
168
+ const ENDPOINT = '/api/${feature.kebab}/create-${feature.kebab}';
169
+
170
+ const LABELS = { title: 'Title', submit: 'Save', saved: 'Saved', retry: 'Try again' };
171
+
172
+ const calls: { url: string; body: Record<string, unknown> }[] = [];
173
+
174
+ /** One stub, both outcomes: the mount costs seconds, so the network's answer is a switch. */
175
+ let networkFails = false;
176
+
177
+ let mounted: MountedIsland;
178
+
179
+ // The build is a Babel pass plus a browser bundle — seconds, not milliseconds. It lives in
180
+ // \`beforeAll\` with its own timeout because \`test\` takes no third argument: fixtures are resolved
181
+ // per case, so the slow work goes where it can be given one and every case shares the result.
182
+ beforeAll(async () => {
183
+ mounted = await mountIsland({
184
+ build: buildIslands,
185
+ root: APP_ROOT,
186
+ file: ISLAND,
187
+ props: { endpoint: ENDPOINT, locale: 'en', labels: LABELS },
188
+ // What the server rendered inside the island's wrapper. \`mount\` replaces it.
189
+ shell: '<p>Loading</p>',
190
+ globals: {
191
+ fetch: (url: string, init: { body: string }): Promise<{ ok: boolean }> => {
192
+ calls.push({ url, body: JSON.parse(init.body) as Record<string, unknown> });
193
+ // What a browser rejects with when there is no network. Not a response: \`response.ok\`
194
+ // is never read on this path, which is exactly why the form has to catch it.
195
+ return networkFails
196
+ ? Promise.reject(new TypeError('Failed to fetch'))
197
+ : Promise.resolve({ ok: true });
198
+ },
199
+ },
200
+ });
201
+ }, 60_000);
202
+
203
+ // The fake \`document\` is process-global: left installed it reaches every LATER FILE in the run.
204
+ //
205
+ // \`?.\` on a binding the type says is always set: TypeScript's definite-assignment analysis does not
206
+ // cross the \`beforeAll\` closure, so a setup that REJECTED leaves this undefined at run time — and
207
+ // bun runs \`afterAll\` regardless. Unguarded, the build failure is followed by a \`TypeError:
208
+ // undefined is not an object\` that says nothing, and that second line is the one a tail reads.
209
+ // Nothing is skipped by the guard: \`mountIsland\` restores the process itself when a mount throws.
210
+ afterAll(() => {
211
+ mounted?.[Symbol.dispose]();
212
+ });
213
+
214
+ /**
215
+ * One mount, driven as a session: the cases below run in order against the same island, because
216
+ * building the real chunk costs seconds and repeating it per case would pay them for state each
217
+ * case sets up anyway. What each one asserts is independent.
218
+ */
219
+ describe('the ${feature.kebab} form island', () => {
220
+ test('mount replaces the server shell with the editor', () => {
221
+ expect(mounted.find('p')?.getAttribute('data-role')).toBe('status');
222
+ // Solid compiles to real DOM calls; a chunk that fell back to the classic React factory names
223
+ // a global that is not in it, and \`Bun.build\` answers \`success: true\` over that all the same.
224
+ expect(mounted.code).not.toMatch(/\\bReact\\b/);
225
+ });
226
+
227
+ test('the field tracks, and submit posts what was typed', async () => {
228
+ const field: FakeElement | null = mounted.find('input');
229
+ expect(field).not.toBeNull();
230
+ if (field !== null) field.value = 'First ${feature.camel}';
231
+ // \`false\` means no handler ran — an island whose onInput never reached the DOM looks
232
+ // identical to a selector typo otherwise.
233
+ expect(mounted.fire(field, 'input')).toBe(true);
234
+ expect(mounted.fire('form', 'submit', { preventDefault: () => {} })).toBe(true);
235
+ await Promise.resolve();
236
+
237
+ expect(calls).toEqual([{ url: ENDPOINT, body: { title: 'First ${feature.camel}' } }]);
238
+ });
239
+
240
+ test('the status line answers the response', () => {
241
+ // The signal reached the DOM: an eager JSX factory renders '' here and never runs again.
242
+ expect(mounted.text('[data-role="status"]')).toBe(LABELS.saved);
243
+ });
244
+
245
+ test('a request that never got a response still reaches retry', async () => {
246
+ // The outcome \`retry\` is FOR. A \`fetch\` that rejects reaches no \`response.ok\`, so without
247
+ // the catch in \`send\` the status line stays on its last value and the rejection escapes.
248
+ networkFails = true;
249
+ expect(mounted.fire('form', 'submit', { preventDefault: () => {} })).toBe(true);
250
+ await Promise.resolve();
251
+ await Promise.resolve();
252
+
253
+ expect(mounted.text('[data-role="status"]')).toBe(LABELS.retry);
254
+ });
255
+ });
256
+ `;
257
+
258
+ /**
259
+ * The slice's form, as the one client shape: `<dir>/<feature>-form.island.tsx` plus its test.
260
+ *
261
+ * `pageDir` is the directory of the page that declares it — the caller's, because only the caller
262
+ * runs both generators and knows where the other one put its file.
263
+ */
264
+ export function formIslandFiles(
265
+ feature: NameSet,
266
+ dir: string,
267
+ pageDir: string,
268
+ ): readonly GeneratedFile[] {
269
+ return [
270
+ {
271
+ path: `${dir}/${feature.kebab}-form.island.tsx`,
272
+ contents: formIslandSource(feature, formIslandSpecifier(feature, dir, pageDir)),
273
+ },
274
+ {
275
+ path: `${dir}/${feature.kebab}-form.island.test.ts`,
276
+ contents: formIslandTest(feature, dir),
277
+ },
278
+ ];
279
+ }
@@ -13,7 +13,8 @@ import type { GeneratedFile, NameSet } from './naming';
13
13
  import { names, pascal } from './naming';
14
14
  import { policyFiles } from './policy';
15
15
  import { queryFiles } from './query';
16
- import { routeFiles } from './route';
16
+ import { formIslandFiles } from './resource-form-island';
17
+ import { routeDir, routeFiles } from './route';
17
18
 
18
19
  const serviceSource = (
19
20
  feature: NameSet,
@@ -142,40 +143,6 @@ export function ${feature.pascal}Card(props: ${feature.pascal}CardProps) {${tran
142
143
  }
143
144
  `;
144
145
 
145
- const formSource = (
146
- feature: NameSet,
147
- module: string | undefined,
148
- ): string => `// Presentation only: the mutator this submits to owns validation server-side, so this form
149
- // never re-implements the invariant — a blank title fails at the boundary, not in the DOM.
150
-
151
- ${catalogImport(module)}
152
- import { createSignal } from 'solid-js';
153
- import styles from '../ui.module.scss';
154
-
155
- export interface ${feature.pascal}FormProps {
156
- readonly onSubmit: (title: string) => void;
157
- }
158
-
159
- export function ${feature.pascal}Form(props: ${feature.pascal}FormProps) {
160
- const [title, setTitle] = createSignal('');${translatorBinding(module)}
161
- return (
162
- <form
163
- class={styles.item}
164
- onSubmit={(event) => {
165
- event.preventDefault();
166
- props.onSubmit(title());
167
- }}
168
- >
169
- <label>
170
- {t('app.${feature.kebab}.titleLabel')}
171
- <input value={title()} onInput={(event) => setTitle(event.currentTarget.value)} />
172
- </label>
173
- <button type="submit">{t('app.${feature.kebab}.submit')}</button>
174
- </form>
175
- );
176
- }
177
- `;
178
-
179
146
  // `admin.<feature>.title` is always here, `--admin` or not: `defineAdmin()` resolves that key the
180
147
  // moment anyone writes the override, and a missing key renders ⟦key⟧ and fails the i18n gate,
181
148
  // while an unused key is only ever reported (`auditCatalogs` fails on `missing`, never `unused`).
@@ -185,6 +152,8 @@ const catalogSource = (feature: NameSet): string =>
185
152
  [`app.${feature.kebab}.updated`]: 'Last updated',
186
153
  [`app.${feature.kebab}.titleLabel`]: 'Title',
187
154
  [`app.${feature.kebab}.submit`]: 'Save',
155
+ [`app.${feature.kebab}.saved`]: 'Saved',
156
+ [`app.${feature.kebab}.retry`]: 'Try again',
188
157
  [`admin.${feature.kebab}.title`]: pascal(feature.plural),
189
158
  });
190
159
 
@@ -204,6 +173,10 @@ export function resourceFiles(rawName: string, target: ResourceOptions): readonl
204
173
  const feature = names(rawName);
205
174
  const slice: FeatureTarget = { surfaceDir: target.surfaceDir, feature: feature.kebab };
206
175
  const dir = `${slice.surfaceDir}/${slice.feature}`;
176
+ // The page this same call writes, from the function that decides where a route goes — the island
177
+ // specifier is resolved against it, so re-deriving the path here would be two answers to one
178
+ // question and only one of them reaches `routeFiles`.
179
+ const pageDir = routeDir('app', feature.pluralKebab);
207
180
  const locales = resolveLocales(target.locales);
208
181
  return [
209
182
  ...entityFiles(rawName, slice),
@@ -220,18 +193,24 @@ export function resourceFiles(rawName: string, target: ResourceOptions): readonl
220
193
  path: `${dir}/ui/${feature.kebab}-card.tsx`,
221
194
  contents: cardSource(feature, target.catalogModule),
222
195
  },
223
- {
224
- path: `${dir}/ui/${feature.kebab}-form.tsx`,
225
- contents: formSource(feature, target.catalogModule),
226
- },
196
+ ...formIslandFiles(feature, dir, pageDir),
227
197
  ...locales.map((locale) => ({
228
198
  path: catalogPath(locale),
229
199
  contents: catalogSource(feature),
230
200
  merge: 'json' as const,
231
201
  })),
232
- // Always an app route: a slice ships a live query, a form and actions, and `generate()`
202
+ // Always an app route: a slice ships a live query, a form island and actions, and `generate()`
233
203
  // refuses `--surface site` for a resource rather than emit them behind a 0kb budget.
234
- ...routeFiles(feature.pluralKebab, { surface: 'app', locales }),
204
+ //
205
+ // `catalogModule` travels with it. It did not, so the page a RESOURCE writes imported `t` from
206
+ // `@ultimat3/i18n` while the components beside it imported `useT` from the app's own catalog
207
+ // module — one command, two idioms, and the framework-import one is issue #249's: it renders
208
+ // strings while depending on nothing that registers them.
209
+ ...routeFiles(feature.pluralKebab, {
210
+ surface: 'app',
211
+ locales,
212
+ ...(target.catalogModule === undefined ? {} : { catalogModule: target.catalogModule }),
213
+ }),
235
214
  ...(target.admin === true ? adminFiles(rawName, slice) : []),
236
215
  ];
237
216
  }
@@ -19,6 +19,14 @@ export type Surface = 'site' | 'app';
19
19
  * that was absent from `x routes`, from the manifest and from `budgets`. Ship the mode that works.
20
20
  */
21
21
  const RENDER: Record<Surface, string> = { site: 'isr', app: 'ssr' };
22
+ /**
23
+ * `app` is `'visible'` on a page the generator writes with no island, and that is not an oversight.
24
+ * A declared strategy costs nothing when there is nothing to hydrate — `hydrateRuntime` is emitted
25
+ * per island DIRECTIVE and answers `''` for a page with none (`packages/render/src/hydrate.ts`), so
26
+ * `x routes` reads `visible` while the document ships 0 bytes. What it buys is that adding the
27
+ * first island is one edit: a declared `'never'` beside an island is `X_ISLAND_NOT_HYDRATED`, which
28
+ * `defineRoute` refuses on purpose, so the scaffold would break at the moment it is first used.
29
+ */
22
30
  const HYDRATE: Record<Surface, string> = { site: 'never', app: 'visible' };
23
31
  const OFFLINE: Record<Surface, string> = { site: 'precache', app: 'runtime' };
24
32
  /**
@@ -66,7 +74,12 @@ export const routeParams = (path: string): readonly string[] =>
66
74
  return name === undefined ? [] : [name];
67
75
  });
68
76
 
69
- const routeDir = (surface: Surface, path: string): string =>
77
+ /**
78
+ * Where the page lands. Exported because an island's `src` is resolved relative to the PAGE, and
79
+ * only this function knows where `x g resource`'s page went — a second copy of the formula in the
80
+ * resource generator is how the specifier it prints stops matching the file it writes.
81
+ */
82
+ export const routeDir = (surface: Surface, path: string): string =>
70
83
  `apps/web/${surface}/${segmentsOf(path).join('/')}`;
71
84
 
72
85
  /**
@@ -125,7 +138,7 @@ export function ${name}Page() {${translatorBinding(module)}
125
138
  };
126
139
 
127
140
  const styleSource =
128
- (): string => `// Semantic tokens only — a raw hex here is a dark-theme bug and a lint failure.
141
+ (): string => `// Semantic tokens only — a raw hex here is refused by the boundaries step (guards/raw-colour.ts), not by lint — a dark-theme bug in every scheme but the one it was written in.
129
142
  @use '@ultimat3/ui/tokens' as tokens;
130
143
 
131
144
  .page {
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { GeneratedFile, NameSet } from './naming';
6
6
  import { apiFiles } from './scaffold-api';
7
+ import { entryFiles } from './scaffold-entries';
7
8
  import { icon } from './scaffold-icon';
8
9
  import { rolesFiles } from './scaffold-roles';
9
10
 
@@ -136,6 +137,8 @@ export const config = defineRoute({
136
137
  // therefore failed x routes with X_ROUTE_MODE_INVALID on the first run, printing a fix nobody
137
138
  // could follow. Ship the mode that works. Async data needs no boundary: await it in the page.
138
139
  render: 'ssr',
140
+ // Stated with no island on the page, deliberately and for free — \`apps/admin/app/admin/page.tsx\`
141
+ // carries the reason.
139
142
  hydrate: 'visible',
140
143
  offline: 'runtime',
141
144
  // Auth is a policy, never a route-local flag: one authz system, evaluated everywhere.
@@ -325,10 +328,16 @@ import { defineRoute } from '@ultimat3/render';
325
328
 
326
329
  export const config = defineRoute({
327
330
  render: 'ssr',
331
+ // Stated on a page whose body is one \`<h1>\`, and it costs nothing: the hydration runtime is
332
+ // emitted per island DIRECTIVE, so \`hydrateRuntime([])\` is \`''\` and this document ships 0 bytes
333
+ // (\`packages/render/src/hydrate.ts\`). It buys the first island being ONE file's edit —
334
+ // \`hydrate: 'never'\` beside an island is \`X_ISLAND_NOT_HYDRATED\`, which defineRoute refuses.
328
335
  hydrate: 'idle',
329
336
  offline: 'network-only',
330
- // Behind auth, and \`ssr\` is the one mode that can be: it renders per request, so the guard runs
331
- // on the server before the page does. \`static\` and \`isr\` refuse a policy outright.
337
+ // Behind auth, so the mode has to render per request: \`ssr\` and \`stream\` both do, and both take
338
+ // a \`policy\`. \`static\` and \`isr\` refuse one outright — a file on disk has no actor to decide
339
+ // against, and an ISR document is cached per URL, so the first actor's HTML would be served to
340
+ // every later one who passes the same policy.
332
341
  policy: { permission: 'admin:read' },
333
342
  budget: { js: '120kb' },
334
343
  meta: ({ t }) => ({ title: t('admin.home.title'), description: t('admin.home.description') }),
@@ -341,80 +350,6 @@ export function AdminHome() {
341
350
  }
342
351
  `;
343
352
 
344
- // The two entry files a deploy needs. Both are deliberately thin: which role a container is, which
345
- // port it binds, how it drains and what a static build enumerates are the framework's answers, so
346
- // an upgrade moves them without a codemod in every app that ever shipped.
347
-
348
- const server =
349
- (): string => `// The production entry. \`docker/Dockerfile\` starts this, and \`x build --target binary\` compiles it.
350
- // ROLE selects what this process is — web, sync, worker, scheduler, replicator, or migrate, which
351
- // applies the migrations and exits. PORT is bound on every interface, because a container bound to
352
- // localhost is unreachable through its own port mapping.
353
-
354
- import { join } from 'node:path';
355
- import { runRole } from '@ultimat3/cli';
356
-
357
- // MORE THAN ONE REPLICA? Add these two lines, above \`runRole\`:
358
- //
359
- // import { configureIdempotency } from '@ultimat3/action';
360
- // configureIdempotency({ scope: 'shared' });
361
- //
362
- // \`idempotent: true\` on an action promises that a retry does not repeat the work. Under the
363
- // process-scoped default that promise holds inside ONE process — a client retrying
364
- // \`POST /api/payments/charge\` after a timeout lands on another replica, which has never seen the
365
- // key, and charges the card twice, silently, with \`x verify\` green. Declaring \`'shared'\` is what
366
- // makes that a boot error (\`X_IDEMPOTENCY_NOT_SHARED\`) unless a shared store is installed.
367
- // \`runRole\` installs the Postgres one for you, on the connection it resolved from \`DATABASE_URL\`,
368
- // so the declaration is all this app owes. It must run before \`runRole\` imports the actions.
369
-
370
- /**
371
- * Where the app is. From this file normally — the image's WORKDIR is not the app root's business.
372
- * A \`--compile\` binary is the exception: its \`import.meta.dir\` is Bun's virtual filesystem, which
373
- * holds this module's bundled imports and none of the app's source, and the framework's registries
374
- * are filled by scanning that source at boot. So a binary reads its root from the directory it is
375
- * started in — it is a launcher for an app tree, not a self-contained copy of one.
376
- */
377
- const root = import.meta.dir.startsWith('/$bunfs')
378
- ? process.cwd()
379
- : join(import.meta.dir, '..', '..');
380
-
381
- // Guarded, because the framework's module scan imports every file under apps/*/ to fill its
382
- // registries — an unguarded boot would start a server inside \`x verify\`.
383
- if (import.meta.main) {
384
- await runRole({ root, env: Bun.env });
385
- }
386
- `;
387
-
388
- const prerender =
389
- (): string => `// The static entry. \`x build --target static\` runs this with \`--out <dir>\` and it writes one HTML
390
- // file per \`render: 'static'\` route — a CDN or an object store then serves site/ with no process
391
- // behind it. Every other render mode needs a running app and is reported as skipped, never emitted.
392
- //
393
- // Skipped is not unweighed: a route that declares a \`budget:\` is rendered in memory and measured
394
- // whatever its mode, so \`x verify\`'s \`budgets\` step has a number for it. \`unmeasured\` is the list
395
- // this build could not render — each one is an X_BUDGET_UNMEASURED at the gate, and this is where
396
- // the reason is.
397
-
398
- import { join } from 'node:path';
399
- import { prerenderSite } from '@ultimat3/cli';
400
-
401
- const root = join(import.meta.dir, '..', '..');
402
- const flag = Bun.argv.indexOf('--out');
403
- const out = (flag === -1 ? undefined : Bun.argv[flag + 1]) ?? join(root, '.x', 'static');
404
- // SITE_ORIGIN is what canonical and og:url are built from; the default is only ever a local build.
405
- // Property access, not \`Bun.env['SITE_ORIGIN']\`: the scaffolded tsconfig does not set
406
- // \`noPropertyAccessFromIndexSignature\`, so the bracket form is the one biome's useLiteralKeys
407
- // reports — a diagnostic in an app's first lint run over a file the app never wrote.
408
- const origin = Bun.env.SITE_ORIGIN;
409
-
410
- if (import.meta.main) {
411
- const report = await prerenderSite({ root, out, ...(origin === undefined ? {} : { origin }) });
412
- await Bun.stdout.write(
413
- \`\${JSON.stringify({ ok: true, out: report.out, pages: report.pages.length, skipped: report.skipped, unmeasured: report.unmeasured })}\\n\`,
414
- );
415
- }
416
- `;
417
-
418
353
  const placeholder = (surface: string, app: NameSet): string => `# ${surface}
419
354
 
420
355
  Placeholder. The monorepo shape exists now so adding ${surface} later is a new directory, not a
@@ -433,8 +368,8 @@ export function appFiles(app: NameSet, example: boolean): readonly GeneratedFile
433
368
  return [
434
369
  { path: 'apps/web/package.json', contents: webPackage(app) },
435
370
  { path: 'apps/web/tsconfig.json', contents: tsconfig() },
436
- { path: 'apps/web/server.ts', contents: server() },
437
- { path: 'apps/web/prerender.ts', contents: prerender() },
371
+ // The process a container starts and the artifact a CDN is handed — `scaffold-entries.ts`.
372
+ ...entryFiles(),
438
373
  { path: 'apps/web/site/icon.png', contents: icon() },
439
374
  { path: 'apps/web/site/page.tsx', contents: sitePage(app) },
440
375
  { path: 'apps/web/site/page.module.scss', contents: siteStyle() },
@@ -52,8 +52,10 @@ ENV NODE_ENV=production \\
52
52
  # own port mapping, its load balancer and every health probe alike.
53
53
  EXPOSE 3000
54
54
 
55
- # Every role serves /healthz and /readyz. /readyz flips to 503 on SIGTERM *before* the socket
56
- # closes, so a rolling restart drains in-flight work instead of dropping it.
55
+ # The probe for the roles that SERVE HTTP — \`web\` and \`sync\`. /readyz flips to 503 on SIGTERM
56
+ # *before* the socket closes, so a rolling restart drains in-flight work instead of dropping it.
57
+ # Every other role opens the scrape listener alone and never binds $PORT, so each one overrides
58
+ # this in docker/docker-compose.prod.yml rather than reporting \`unhealthy\` for its whole life.
57
59
  HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 CMD \\
58
60
  bun --eval "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/readyz').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"
59
61
 
@@ -120,6 +122,22 @@ x-image: &image
120
122
  depends_on:
121
123
  db: { condition: service_healthy }
122
124
 
125
+ # The image's own HEALTHCHECK fetches \`/readyz\` on $PORT, and only \`web\` and \`sync\` open an HTTP
126
+ # socket — every other role gets the scrape listener and nothing else. A service that inherits that
127
+ # probe is fetching a port it never binds: it reports \`unhealthy\` for its whole life and anything
128
+ # gated on it never starts. Probes follow the role here, exactly as they do in \`docker/helm\`.
129
+ x-metrics-probe: &metrics-probe
130
+ test: ['CMD', 'bun', '--eval', "fetch('http://127.0.0.1:'+(process.env.METRICS_PORT||9090)+'/metrics').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
131
+ interval: 10s
132
+ timeout: 3s
133
+ start_period: 30s
134
+ retries: 3
135
+
136
+ # Run-once services exit. A probe against an exited container reports \`unhealthy\` forever, and
137
+ # nothing waits on their health — \`service_completed_successfully\` is what the others gate on.
138
+ x-run-once-probe: &run-once-probe
139
+ disable: true
140
+
123
141
  services:
124
142
  db:
125
143
  image: postgres:17-alpine
@@ -138,6 +156,7 @@ services:
138
156
  <<: *image
139
157
  environment: [ROLE=migrate]
140
158
  restart: 'no'
159
+ healthcheck: *run-once-probe
141
160
 
142
161
  # Run-once, AFTER the new version serves. Deliberately NOT part of the release gate: a slow
143
162
  # UPDATE there holds the deploy open against a database still serving the previous version.
@@ -158,6 +177,7 @@ services:
158
177
  # listing this last would only look like "after". The image's HEALTHCHECK is what makes it true.
159
178
  web: { condition: service_healthy }
160
179
  restart: 'no'
180
+ healthcheck: *run-once-probe
161
181
 
162
182
  web:
163
183
  <<: *image
@@ -184,6 +204,7 @@ services:
184
204
  depends_on:
185
205
  db: { condition: service_healthy }
186
206
  migrate: { condition: service_completed_successfully }
207
+ healthcheck: *metrics-probe
187
208
  deploy: { replicas: 1 } # scales on queue depth
188
209
 
189
210
  scheduler:
@@ -192,7 +213,12 @@ services:
192
213
  depends_on:
193
214
  db: { condition: service_healthy }
194
215
  migrate: { condition: service_completed_successfully }
195
- deploy: { replicas: 1 } # fixed 1; leadership is a Postgres advisory lock
216
+ # Fixed 1. Leadership is an EXPIRING LEASE ROW in \`x_scheduler_leader\` (dev-roles.ts,
217
+ # driver-pg-ddl.ts), NOT an advisory lock: that grant belongs to the session, not to the
218
+ # process — it outlives every transaction and no pooled node can renew it or prove it still
219
+ # holds one. A second instance is harmless but idle.
220
+ healthcheck: *metrics-probe
221
+ deploy: { replicas: 1 }
196
222
 
197
223
  volumes:
198
224
  pgdata:
@@ -208,7 +234,7 @@ nothing to rebuild between staging and production.
208
234
  | \`web\` | HTTP: pages, actions, assets | \`$PORT\` (default 3000) |
209
235
  | \`sync\` | websockets for live queries | \`$PORT + 1\` |
210
236
  | \`worker\` | the job queue | — |
211
- | \`scheduler\` | cron tasks; leadership is a Postgres advisory lock | — |
237
+ | \`scheduler\` | cron tasks; leadership is an expiring lease row in \`x_scheduler_leader\` | — |
212
238
  | \`replicator\` | the logical replication slot, exactly one per database | — |
213
239
  | \`migrate\` | applies pending migrations and **exits** | — |
214
240