@ultimat3/cli 22.15.0 → 24.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/CLAUDE.md +36 -13
  2. package/README.md +31 -8
  3. package/package.json +29 -28
  4. package/src/admin-catalog-keys.ts +16 -0
  5. package/src/admin-registration.ts +192 -0
  6. package/src/affected.ts +27 -35
  7. package/src/api-registration.ts +125 -11
  8. package/src/app-artifacts.ts +1 -1
  9. package/src/app-auth.ts +4 -27
  10. package/src/app-boundaries.ts +6 -20
  11. package/src/app-config-load.ts +51 -0
  12. package/src/app-env.ts +5 -11
  13. package/src/app-load.ts +55 -11
  14. package/src/app-manifest.ts +16 -8
  15. package/src/app-mcp.ts +10 -23
  16. package/src/app-permissions-borrowed.ts +129 -0
  17. package/src/app-permissions-site.ts +24 -0
  18. package/src/app-permissions.ts +0 -0
  19. package/src/app-reload-graph.ts +7 -30
  20. package/src/app-transport.ts +219 -0
  21. package/src/boundary-findings.ts +16 -4
  22. package/src/browser-transport.ts +160 -0
  23. package/src/budgets.ts +27 -34
  24. package/src/cdp-shot-driver.ts +3 -1
  25. package/src/cmd-auth-spec.ts +16 -0
  26. package/src/cmd-auth.ts +65 -0
  27. package/src/cmd-build-spec.ts +10 -3
  28. package/src/cmd-build.ts +107 -11
  29. package/src/cmd-db-backfill.ts +25 -12
  30. package/src/cmd-db.ts +66 -16
  31. package/src/cmd-deploy-helm.ts +7 -6
  32. package/src/cmd-deploy.ts +4 -4
  33. package/src/cmd-dev.ts +9 -8
  34. package/src/cmd-doctor-spec.ts +2 -9
  35. package/src/cmd-doctor.ts +75 -24
  36. package/src/cmd-env.ts +26 -2
  37. package/src/cmd-generate.ts +195 -47
  38. package/src/cmd-help.ts +19 -6
  39. package/src/cmd-i18n.ts +10 -9
  40. package/src/cmd-jobs-spec.ts +19 -3
  41. package/src/cmd-jobs.ts +136 -7
  42. package/src/cmd-manifest-spec.ts +1 -1
  43. package/src/cmd-manifest.ts +6 -25
  44. package/src/cmd-mcp.ts +46 -15
  45. package/src/cmd-new.ts +15 -1
  46. package/src/cmd-planned.ts +15 -8
  47. package/src/cmd-routes.ts +27 -3
  48. package/src/cmd-secrets-ring.ts +76 -0
  49. package/src/cmd-secrets-spec.ts +10 -2
  50. package/src/cmd-secrets.ts +95 -18
  51. package/src/cmd-shot-matrix.ts +1 -3
  52. package/src/cmd-shot.ts +12 -3
  53. package/src/cmd-tasks.ts +84 -15
  54. package/src/cmd-test-spec.ts +1 -7
  55. package/src/cmd-test.ts +13 -29
  56. package/src/cmd-verify.ts +13 -5
  57. package/src/comment-safe.ts +16 -0
  58. package/src/coverage-floor.ts +334 -0
  59. package/src/coverage-lcov.ts +193 -0
  60. package/src/coverage-source.ts +136 -0
  61. package/src/db-object-drift.ts +30 -0
  62. package/src/db-replay-engine.ts +115 -0
  63. package/src/db-schema-dump.ts +150 -0
  64. package/src/db-schema-refresh.ts +122 -0
  65. package/src/db-subscribes.ts +12 -4
  66. package/src/dev-dashboard-guard.ts +76 -0
  67. package/src/dev-dashboard.ts +51 -29
  68. package/src/dev-lock.ts +4 -7
  69. package/src/dev-route-table.ts +9 -0
  70. package/src/dispatch.ts +3 -6
  71. package/src/doctor-app-url.ts +39 -0
  72. package/src/doctor-auth.ts +95 -0
  73. package/src/doctor-coverage.ts +39 -0
  74. package/src/doctor-guards.ts +54 -0
  75. package/src/doctor-sealed.ts +132 -0
  76. package/src/document-graph.ts +82 -0
  77. package/src/drift-replica-identity.ts +42 -0
  78. package/src/error-catalog.ts +6 -9
  79. package/src/error-codes.ts +55 -3
  80. package/src/error-unthrown.ts +29 -11
  81. package/src/errors.ts +54 -83
  82. package/src/framework-schema-apply.ts +78 -0
  83. package/src/framework-schema-stamp.ts +96 -0
  84. package/src/framework-schema.ts +28 -2
  85. package/src/generate-files.ts +79 -12
  86. package/src/generate-grant-findings.ts +76 -0
  87. package/src/generate-grants.ts +77 -3
  88. package/src/generate-kinds.ts +52 -1
  89. package/src/generate-shadow.ts +46 -0
  90. package/src/generate-write.ts +62 -40
  91. package/src/generated-imports.ts +59 -0
  92. package/src/gh-target.ts +12 -7
  93. package/src/guard-sources.ts +104 -0
  94. package/src/guards.ts +23 -7
  95. package/src/handle-registration.ts +217 -0
  96. package/src/i18n-audit.ts +4 -1
  97. package/src/i18n-registration.ts +36 -0
  98. package/src/image-prepare.ts +51 -8
  99. package/src/import-closure.ts +165 -0
  100. package/src/index.ts +12 -5
  101. package/src/island-bundle.ts +3 -15
  102. package/src/island-store.ts +24 -16
  103. package/src/jobs-drain.ts +95 -21
  104. package/src/jobs-json.ts +50 -0
  105. package/src/jobs-report.ts +7 -4
  106. package/src/json-merge.ts +46 -0
  107. package/src/loopback-host.ts +13 -0
  108. package/src/manifest-staleness.ts +62 -0
  109. package/src/mcp-errors.ts +41 -4
  110. package/src/mcp-host.ts +16 -9
  111. package/src/mcp-test-run.ts +38 -0
  112. package/src/messages.ts +35 -0
  113. package/src/metrics-endpoint.ts +95 -24
  114. package/src/migration-extensions.ts +29 -0
  115. package/src/module-imports.ts +47 -0
  116. package/src/output.ts +24 -9
  117. package/src/page-navigation.ts +7 -23
  118. package/src/page-speculation.ts +5 -20
  119. package/src/permission-grants.ts +41 -9
  120. package/src/prerender-out.ts +34 -7
  121. package/src/prerender.ts +8 -5
  122. package/src/publish-closure.ts +101 -0
  123. package/src/pwa-artifacts.ts +26 -94
  124. package/src/registry.ts +2 -0
  125. package/src/revalidated-response.ts +4 -3
  126. package/src/role-load.ts +143 -0
  127. package/src/role-replicator.ts +16 -0
  128. package/src/role-start.ts +27 -5
  129. package/src/role-sync.ts +55 -20
  130. package/src/role-wake.ts +45 -0
  131. package/src/runtime-admin.ts +141 -0
  132. package/src/runtime-assets.ts +11 -7
  133. package/src/runtime-bindings.ts +4 -4
  134. package/src/runtime-cache.ts +38 -47
  135. package/src/runtime-hooks.ts +5 -2
  136. package/src/runtime-idempotency-scope.ts +23 -0
  137. package/src/runtime-isr.ts +36 -0
  138. package/src/runtime-jobs.ts +8 -33
  139. package/src/runtime-mfa-warning.ts +29 -0
  140. package/src/runtime-notify-retention.ts +10 -46
  141. package/src/runtime-overrides.ts +1 -1
  142. package/src/runtime-purge.ts +38 -16
  143. package/src/runtime-queue.ts +39 -11
  144. package/src/runtime-realtime.ts +9 -45
  145. package/src/runtime-render.ts +68 -35
  146. package/src/runtime-services.ts +22 -18
  147. package/src/runtime-storage.ts +10 -5
  148. package/src/scaffold-typecheck.ts +1 -0
  149. package/src/schema-diff.ts +16 -0
  150. package/src/schema-drift.ts +45 -15
  151. package/src/schema-dump-drift.ts +64 -0
  152. package/src/seo-routes.ts +80 -9
  153. package/src/serve-boot.ts +114 -158
  154. package/src/serve-drain.ts +9 -33
  155. package/src/serve-env.ts +9 -6
  156. package/src/serve-prebuilt-paths.ts +19 -0
  157. package/src/serve-prebuilt.ts +84 -0
  158. package/src/serve-web.ts +169 -0
  159. package/src/serve.ts +6 -9
  160. package/src/server-barrels.ts +54 -0
  161. package/src/shot-locale.ts +5 -23
  162. package/src/site-assets.ts +3 -5
  163. package/src/site-config.ts +9 -48
  164. package/src/site-seo.ts +1 -1
  165. package/src/sitemap-lastmod.ts +5 -0
  166. package/src/solid-loader.ts +12 -3
  167. package/src/source-edit.ts +49 -0
  168. package/src/source-files.ts +6 -0
  169. package/src/stored-object-headers.ts +43 -0
  170. package/src/sync-url.ts +2 -2
  171. package/src/tasks-facts.ts +3 -26
  172. package/src/templates/action.ts +147 -11
  173. package/src/templates/admin-catalog.ts +58 -0
  174. package/src/templates/admin-page.ts +36 -8
  175. package/src/templates/admin.ts +57 -17
  176. package/src/templates/backfill.ts +121 -87
  177. package/src/templates/entity.ts +140 -44
  178. package/src/templates/guard-animated-layout-property.ts +4 -11
  179. package/src/templates/guard-bare-error.ts +9 -13
  180. package/src/templates/guard-focus-visible.ts +4 -11
  181. package/src/templates/guard-image-dimensions.ts +6 -6
  182. package/src/templates/guard-island-without-states.ts +2 -4
  183. package/src/templates/guard-raw-breakpoint.ts +210 -0
  184. package/src/templates/guard-raw-colour.ts +4 -11
  185. package/src/templates/guard-raw-length.ts +375 -0
  186. package/src/templates/guard-raw-motion.ts +213 -0
  187. package/src/templates/guard-raw-shadow.ts +187 -0
  188. package/src/templates/guard-raw-z-index.ts +166 -0
  189. package/src/templates/guard-repo-raw-sql.ts +345 -0
  190. package/src/templates/guard-semantic-interactive.ts +6 -6
  191. package/src/templates/guard-undeclared-custom-property.ts +217 -0
  192. package/src/templates/guard-undefined-style-class.ts +215 -0
  193. package/src/templates/guard-untranslated-string.ts +6 -6
  194. package/src/templates/guard-unzoned-date.ts +9 -13
  195. package/src/templates/index.ts +3 -2
  196. package/src/templates/island.ts +34 -51
  197. package/src/templates/job-body-test.ts +121 -0
  198. package/src/templates/job.ts +23 -8
  199. package/src/templates/naming.ts +28 -0
  200. package/src/templates/policy.ts +16 -9
  201. package/src/templates/query.ts +242 -26
  202. package/src/templates/resource-create.ts +62 -18
  203. package/src/templates/resource-form-island.ts +58 -80
  204. package/src/templates/resource-page.ts +269 -0
  205. package/src/templates/resource-service.ts +96 -0
  206. package/src/templates/resource.ts +86 -61
  207. package/src/templates/route.ts +41 -35
  208. package/src/templates/scaffold-api.ts +55 -1
  209. package/src/templates/scaffold-app.ts +177 -53
  210. package/src/templates/scaffold-auth.ts +83 -19
  211. package/src/templates/scaffold-browser-client.ts +90 -0
  212. package/src/templates/scaffold-container-compose.ts +10 -1
  213. package/src/templates/scaffold-container.ts +20 -2
  214. package/src/templates/scaffold-dashboard-bare.ts +64 -2
  215. package/src/templates/scaffold-dashboard-example.ts +124 -31
  216. package/src/templates/scaffold-dashboard-shared.ts +1 -20
  217. package/src/templates/scaffold-dashboard.ts +1 -2
  218. package/src/templates/scaffold-db-client.ts +144 -0
  219. package/src/templates/scaffold-db-package.ts +90 -8
  220. package/src/templates/scaffold-docs.ts +50 -5
  221. package/src/templates/scaffold-guards.ts +54 -11
  222. package/src/templates/scaffold-helm-fences.ts +76 -0
  223. package/src/templates/scaffold-helm-templates.ts +32 -7
  224. package/src/templates/scaffold-helm.ts +31 -1
  225. package/src/templates/scaffold-http.ts +9 -7
  226. package/src/templates/scaffold-repo.ts +61 -3
  227. package/src/templates/scaffold-roles.ts +56 -9
  228. package/src/templates/scaffold-shell.ts +74 -105
  229. package/src/templates/scaffold-site.ts +45 -19
  230. package/src/templates/scaffold-ui-package.ts +18 -0
  231. package/src/templates/slice-foundation.ts +46 -8
  232. package/src/test-counts.ts +14 -2
  233. package/src/test-ignores.ts +25 -0
  234. package/src/test-passes.ts +0 -15
  235. package/src/test-select.ts +5 -14
  236. package/src/test-shards.ts +41 -93
  237. package/src/test-workers.ts +26 -4
  238. package/src/theme-boot.ts +6 -31
  239. package/src/transport-calls.ts +105 -0
  240. package/src/trusted-hops-error.ts +17 -0
  241. package/src/unscanned-admin.ts +54 -0
  242. package/src/unthrown-codes.ts +26 -0
  243. package/src/verify-checks.ts +43 -53
  244. package/src/verify-coverage-run.ts +190 -0
  245. package/src/verify-deadline.ts +353 -0
  246. package/src/verify-floor.ts +36 -9
  247. package/src/verify-merge.ts +171 -45
  248. package/src/verify-part-step.ts +90 -0
  249. package/src/verify-progress.ts +25 -0
  250. package/src/verify-role-load-probe.ts +54 -0
  251. package/src/verify-role-load.ts +148 -0
  252. package/src/verify-run.ts +116 -37
  253. package/src/verify-shard.ts +33 -11
  254. package/src/verify-stalled.ts +152 -0
  255. package/src/verify-step.ts +42 -1
  256. package/src/verify-tests.ts +83 -8
  257. package/src/workspace-checks.ts +50 -8
  258. package/src/workspace-dep-edit.ts +127 -0
  259. package/src/build-errors.ts +0 -54
  260. package/src/cmd-dev-fixture.ts +0 -204
  261. package/src/dev-live-fixture.ts +0 -122
  262. package/src/policy-fixture.ts +0 -78
  263. package/src/realtime-browser-probe-fixture.ts +0 -9
  264. package/src/role-start-fixture.ts +0 -78
  265. package/src/scaffold-fixture.ts +0 -160
  266. package/src/templates/island-fixture.ts +0 -76
  267. package/src/verify-errors.ts +0 -49
@@ -170,9 +170,9 @@ export function mount(el: HTMLElement, props: ${feature.pascal}FormProps): void
170
170
  const formIslandTest = (
171
171
  feature: NameSet,
172
172
  dir: string,
173
- ): string => `// The form the browser actually runs. \`mountIsland\` builds this entry with the same
174
- // \`buildIslands\` that \`x build\` and \`x dev\` use, imports the emitted chunk the way the hydration
175
- // runtime does, and drives \`mount\` against a DOM small enough to read.
173
+ ): string => `// The form the browser actually runs, in every state its manifest declares: built once for this
174
+ // file with the same \`buildIslands\` that \`x build\` and \`x dev\` use, imported the way the
175
+ // hydration runtime imports it, and driven against a DOM small enough to read.
176
176
  //
177
177
  // It is the test that keeps this file a CLIENT entry. A generated form that only typechecks is
178
178
  // what shipped before: server-rendered, every \`on*\` prop dropped, every signal read once.
@@ -180,76 +180,46 @@ const formIslandTest = (
180
180
  import { join } from 'node:path';
181
181
  import { buildIslands } from '@ultimat3/cli';
182
182
  import {
183
- afterAll,
184
- beforeAll,
185
- describe,
183
+ describeIslandState,
186
184
  expect,
187
185
  type FakeElement,
188
186
  type MountedIsland,
189
- mountIsland,
190
187
  test,
191
188
  } from '@ultimat3/testing';
192
-
193
- const APP_ROOT = join(import.meta.dir, ${upToAppRoot(dir)});
194
- const ISLAND = '${dir}/${feature.kebab}-form.island.tsx';
195
- const ENDPOINT = '/api/${feature.kebab}/create-${feature.kebab}';
196
-
197
- const LABELS = {
198
- title: 'Title',
199
- price: 'Price',
200
- submit: 'Save',
201
- saved: 'Saved',
202
- retry: 'Try again',
203
- };
189
+ import { ${feature.camel}FormStates } from './${feature.kebab}-form.island.states';
204
190
 
205
191
  const calls: { url: string; body: Record<string, unknown> }[] = [];
206
192
 
207
- /** One stub, both outcomes: the mount costs seconds, so the network's answer is a switch. */
193
+ /** One stub, both outcomes: a block shares its mount, so the network's answer is a switch. */
208
194
  let networkFails = false;
209
195
 
210
- let mounted: MountedIsland;
211
-
212
- // The build is a Babel pass plus a browser bundle — seconds, not milliseconds. It lives in
213
- // \`beforeAll\` with its own timeout because \`test\` takes no third argument: fixtures are resolved
214
- // per case, so the slow work goes where it can be given one and every case shares the result.
215
- beforeAll(async () => {
216
- mounted = await mountIsland({
217
- build: buildIslands,
218
- root: APP_ROOT,
219
- file: ISLAND,
220
- props: { endpoint: ENDPOINT, locale: 'en', currency: 'USD', labels: LABELS },
221
- // What the server rendered inside the island's wrapper. \`mount\` replaces it.
222
- shell: '<p>Loading</p>',
223
- globals: {
224
- // The form sends through \`clientTransport\`, which calls \`globalThis.fetch\` — this stub.
225
- fetch: (url: string, init: { body: string }): Promise<Response> => {
226
- calls.push({ url, body: JSON.parse(init.body) as Record<string, unknown> });
227
- // What a browser rejects with when there is no network. Not a response, which is exactly
228
- // why the form has to catch it.
229
- return networkFails
230
- ? Promise.reject(new TypeError('Failed to fetch'))
231
- : Promise.resolve(Response.json({ id: 'created' }));
232
- },
196
+ // One block per declared state: \`describeIslandState\` mounts it before the block's first test
197
+ // and disposes it after the last. The props are the manifest's own — what
198
+ // \`x shot --island ${feature.kebab}-form\` photographs — so the picture and the test are of one
199
+ // component.
200
+ const island = {
201
+ build: buildIslands,
202
+ root: join(import.meta.dir, ${upToAppRoot(dir)}),
203
+ // What the server rendered inside the island's wrapper. \`mount\` replaces it.
204
+ shell: '<p>Loading</p>',
205
+ globals: {
206
+ // The form sends through \`clientTransport\`, which calls \`globalThis.fetch\` — this stub.
207
+ fetch: (url: string, init: { body: string }): Promise<Response> => {
208
+ calls.push({ url, body: JSON.parse(init.body) as Record<string, unknown> });
209
+ // What a browser rejects with when there is no network. Not a response, which is exactly
210
+ // why the form has to catch it.
211
+ return networkFails
212
+ ? Promise.reject(new TypeError('Failed to fetch'))
213
+ : Promise.resolve(Response.json({ id: 'created' }));
233
214
  },
234
- });
235
- }, 60_000);
236
-
237
- // The fake \`document\` is process-global: left installed it reaches every LATER FILE in the run.
238
- //
239
- // \`?.\` on a binding the type says is always set: TypeScript's definite-assignment analysis does not
240
- // cross the \`beforeAll\` closure, so a setup that REJECTED leaves this undefined at run time — and
241
- // bun runs \`afterAll\` regardless. Unguarded, the build failure is followed by a \`TypeError:
242
- // undefined is not an object\` that says nothing, and that second line is the one a tail reads.
243
- // Nothing is skipped by the guard: \`mountIsland\` restores the process itself when a mount throws.
244
- afterAll(() => {
245
- mounted?.[Symbol.dispose]();
246
- });
215
+ },
216
+ };
247
217
 
248
218
  /**
249
219
  * Until the status line changes, a bounded number of macrotasks: the send is several awaits deep
250
220
  * inside \`clientTransport\`, so counting microtasks would pin the transport, not the form.
251
221
  */
252
- async function statusSettled(): Promise<void> {
222
+ async function statusSettled(mounted: MountedIsland): Promise<void> {
253
223
  const before = mounted.text('[data-role="status"]');
254
224
  for (let tick = 0; tick < 50; tick += 1) {
255
225
  if (mounted.text('[data-role="status"]') !== before) return;
@@ -257,53 +227,61 @@ async function statusSettled(): Promise<void> {
257
227
  }
258
228
  }
259
229
 
260
- /**
261
- * One mount, driven as a session: the cases below run in order against the same island, because
262
- * building the real chunk costs seconds and repeating it per case would pay them for state each
263
- * case sets up anyway. What each one asserts is independent.
264
- */
265
- describe('the ${feature.kebab} form island', () => {
230
+ // Each case sets the network's answer itself and asserts on what its own submit produced, so
231
+ // none depends on the one before it.
232
+ describeIslandState(${feature.camel}FormStates, 'idle', island, (mounted) => {
266
233
  test('mount replaces the server shell with the editor', () => {
267
- expect(mounted.find('p')?.getAttribute('data-role')).toBe('status');
234
+ expect(mounted().find('p')?.getAttribute('data-role')).toBe('status');
235
+ expect(mounted().text('button')).toBe('Save');
268
236
  // Solid compiles to real DOM calls; a chunk that fell back to the classic React factory names
269
237
  // a global that is not in it, and \`Bun.build\` answers \`success: true\` over that all the same.
270
- expect(mounted.code).not.toMatch(/\\bReact\\b/);
238
+ expect(mounted().code).not.toMatch(/\\bReact\\b/);
271
239
  });
272
240
 
273
- test('the fields track, and submit posts the create input', async () => {
274
- const title: FakeElement | null = mounted.find('input[aria-label="Title"]');
275
- const price: FakeElement | null = mounted.find('input[aria-label="Price"]');
241
+ test('the fields track, submit posts the create input, and the status answers', async () => {
242
+ networkFails = false;
243
+ const title: FakeElement | null = mounted().find('input[aria-label="Title"]');
244
+ const price: FakeElement | null = mounted().find('input[aria-label="Price"]');
276
245
  expect(title).not.toBeNull();
277
246
  expect(price).not.toBeNull();
278
247
  if (title !== null) title.value = 'First ${feature.camel}';
279
248
  if (price !== null) price.value = '12.50';
280
249
  // \`false\` means no handler ran — an island whose onInput never reached the DOM looks
281
250
  // identical to a selector typo otherwise.
282
- expect(mounted.fire(title, 'input')).toBe(true);
283
- expect(mounted.fire(price, 'input')).toBe(true);
284
- expect(mounted.fire('form', 'submit', { preventDefault: () => {} })).toBe(true);
285
- await statusSettled();
251
+ expect(mounted().fire(title, 'input')).toBe(true);
252
+ expect(mounted().fire(price, 'input')).toBe(true);
253
+ expect(mounted().fire('form', 'submit', { preventDefault: () => {} })).toBe(true);
254
+ await statusSettled(mounted());
286
255
 
287
256
  const body = {
288
257
  title: 'First ${feature.camel}',
289
258
  price: { minor: 1250, currency: 'USD' },
290
259
  };
291
- expect(calls).toEqual([{ url: ENDPOINT, body }]);
292
- });
293
-
294
- test('the status line answers the response', () => {
260
+ // The endpoint is the state's own: the form posts where the server told it to.
261
+ expect(calls.at(-1)).toEqual({ url: '/api/${feature.pluralKebab}/create', body });
295
262
  // The signal reached the DOM: an eager JSX factory renders '' here and never runs again.
296
- expect(mounted.text('[data-role="status"]')).toBe(LABELS.saved);
263
+ expect(mounted().text('[data-role="status"]')).toBe('Saved');
297
264
  });
298
265
 
299
266
  test('a request that never got a response still reaches retry', async () => {
300
267
  // The outcome \`retry\` is FOR. A \`fetch\` that rejects produces no answer, so without the
301
268
  // catch in \`send\` the status line stays on its last value and the rejection escapes.
302
269
  networkFails = true;
303
- expect(mounted.fire('form', 'submit', { preventDefault: () => {} })).toBe(true);
304
- await statusSettled();
270
+ expect(mounted().fire('form', 'submit', { preventDefault: () => {} })).toBe(true);
271
+ await statusSettled(mounted());
272
+
273
+ expect(mounted().text('[data-role="status"]')).toBe('That did not save. Try again.');
274
+ });
275
+ });
305
276
 
306
- expect(mounted.text('[data-role="status"]')).toBe(LABELS.retry);
277
+ describeIslandState(${feature.camel}FormStates, 'long-labels', island, (mounted) => {
278
+ test('every label is the state’s own, in the locale it names', () => {
279
+ // Whether they FIT is a picture's question — \`x shot --island ${feature.kebab}-form\`. What a
280
+ // test can say is that the island was handed this state's text, and none of the idle one's.
281
+ expect(mounted().find('input[aria-label="Bezeichnung des Beitrags"]')).not.toBeNull();
282
+ expect(mounted().find('input[aria-label="Title"]')).toBeNull();
283
+ expect(mounted().text('button')).toBe('Änderungen speichern');
284
+ expect(mounted().documentElement.getAttribute('lang')).toBe('de');
307
285
  });
308
286
  });
309
287
  `;
@@ -0,0 +1,269 @@
1
+ // `x g resource <name>`'s page: the plural route a slice is reached at. It READS — `load` calls the
2
+ // slice's own query as the request's actor — renders the rows through the slice's list component
3
+ // inside `AsyncRegion` (ready, empty, failed), and mounts the slice's form island. `x g route`
4
+ // writes a page with a heading and nothing to read; a resource has rows and a form, and a page
5
+ // that showed neither was a slice nobody could reach without wiring it by hand.
6
+
7
+ import { sortedImports } from './imports';
8
+ import type { GeneratedFile, NameSet } from './naming';
9
+ import { titleKey } from './naming';
10
+ import { formIslandSpecifier } from './resource-form-island';
11
+ import { LINE_WIDTH } from './wrap';
12
+
13
+ /**
14
+ * The page's JS budget — MEASURED, never a ceiling picked to pass. `x build --target static` on a
15
+ * fresh `x new` app with `x g resource widget`, Bun 1.4.0 (CI's pin), 2026-10-01: 58,522 B — the
16
+ * form island 57,667 + the `visible` hydration runtime 855. 64kb is 65,536: the headroom is Bun's
17
+ * own movement on one source — a patch release that keeps core's declared modules measured +3.0 kB
18
+ * on the scaffold's theme island, and the tree-shaker flaps by up to 1.1 kB between runs. An app
19
+ * that adds per-document scripts (`navigation: { client: [...] }` charges its router, ~18 kB, to
20
+ * every `app/` page) is over it on its first build, and `nextSteps` sends the author there.
21
+ */
22
+ export const RESOURCE_PAGE_BUDGET = '64kb';
23
+ export const RESOURCE_PAGE_MEASURED = '58,522 B';
24
+
25
+ /** How the page reaches a string — the same rule `route.ts` states, for the same issue (#249). */
26
+ const catalogImport = (module: string | undefined): string =>
27
+ module === undefined
28
+ ? "import { t } from '@ultimat3/i18n';"
29
+ : `import { useT } from '${module}';`;
30
+
31
+ /** `currentLocale` rides on the framework import when the app has no catalog module. */
32
+ const i18nImports = (module: string | undefined): readonly string[] =>
33
+ module === undefined
34
+ ? ["import { currentLocale, t } from '@ultimat3/i18n';"]
35
+ : [catalogImport(module), "import { currentLocale } from '@ultimat3/i18n';"];
36
+
37
+ /** `const load = … => rowsState(() => <read>(input()));`, as Biome prints it at any name length. */
38
+ const loadLine = (read: string): string => {
39
+ const head = 'const load = (): Promise<PageData> =>';
40
+ const body = `rowsState(() => ${read}(input()));`;
41
+ return `${head} ${body}`.length <= LINE_WIDTH ? `${head} ${body}` : `${head}\n ${body}`;
42
+ };
43
+
44
+ export interface ResourcePageOptions {
45
+ readonly feature: NameSet;
46
+ /** The slice: `apps/web/app/<feature>`. */
47
+ readonly dir: string;
48
+ /** Where the page lands: `apps/web/app/<features>`. */
49
+ readonly pageDir: string;
50
+ readonly catalogModule?: string;
51
+ /** The app has `apps/web/shared/shell.tsx` — every `x new` app does — so the page sits in it. */
52
+ readonly shell: boolean;
53
+ readonly dbModule: string;
54
+ }
55
+
56
+ const pageSource = (options: ResourcePageOptions): string => {
57
+ const { feature } = options;
58
+ const path = feature.pluralKebab;
59
+ const list = `${feature.camel}List`;
60
+ const open = options.shell
61
+ ? '<Shell>\n <div class={styles.page}>'
62
+ : '<main class={styles.page}>';
63
+ const close = options.shell ? ' </div>\n </Shell>' : ' </main>';
64
+ const indent = options.shell ? ' ' : ' ';
65
+ const inner = (lines: string): string =>
66
+ lines
67
+ .split('\n')
68
+ .map((line) => (line === '' ? line : `${indent}${line}`))
69
+ .join('\n');
70
+ return `// /${path}: the org's ${feature.pluralKebab}, newest first, and the form that adds one. \`load\`
71
+ // reads through the slice's own query, so the policy, the tenancy and the order are the query's;
72
+ // the form is the slice's island, the one module of this page a browser downloads.
73
+
74
+ ${sortedImports([
75
+ ...i18nImports(options.catalogModule),
76
+ "import { derivePath } from '@ultimat3/action';",
77
+ "import type { AsyncState } from '@ultimat3/core';",
78
+ "import type { KnownPermission } from '@ultimat3/policy';",
79
+ "import { defineRoute, island } from '@ultimat3/render';",
80
+ "import { AsyncRegion, EmptyState, PageHeader, Section, Skeleton } from '@ultimat3/ui';",
81
+ ])}
82
+ ${options.shell ? "import { Shell } from '../../shared/shell';\n" : ''}import type { ${feature.pascal} } from '../${feature.kebab}/entity';
83
+ import { ${list} } from '../${feature.kebab}/live/${feature.kebab}-list';
84
+ import { ${feature.pascal}List } from '../${feature.kebab}/ui';
85
+ import styles from './page.module.scss';
86
+
87
+ /** One page of rows: the read's own bound. Past it, page with a cursor (\`${list}.page\`). */
88
+ const PAGE_ROWS = 50;
89
+
90
+ /** The currency a price is typed in; the form sends integer minor units of it. */
91
+ const CURRENCY = 'USD';
92
+
93
+ type Row = ${feature.pascal};
94
+
95
+ /** What \`load\` answers: the read's outcome as DATA, so the page renders each of its states. */
96
+ export type PageData = AsyncState<readonly Row[]>;
97
+
98
+ /**
99
+ * The read, as a state. A refusal or a failed read is the region's error branch — code, cause and
100
+ * fix on screen, the form still there — never a 500 for the whole document.
101
+ */
102
+ export async function rowsState(read: () => Promise<readonly Row[]>): Promise<PageData> {
103
+ try {
104
+ return { status: 'ready', data: await read() };
105
+ } catch (error) {
106
+ return { status: 'failed', error };
107
+ }
108
+ }
109
+
110
+ /** No org: the query reads the ACTOR's, so a read can only ever be for the caller's own org. */
111
+ const input = () => ({ limit: PAGE_ROWS });
112
+
113
+ ${loadLine(list)}
114
+
115
+ // Declared ABOVE \`defineRoute\`: the route drains the islands declared before it. Named by
116
+ // SPECIFIER, never imported — a string has no import edge, so the page's bundle graph stays the
117
+ // page's (axiom 6). \`props\` are exactly the form's \`${feature.pascal}FormProps\` keys.
118
+ const ${feature.pascal}Form = island({
119
+ src: '${formIslandSpecifier(feature, options.dir, options.pageDir)}',
120
+ props: ['endpoint', 'locale', 'currency', 'labels'],
121
+ });
122
+
123
+ export const config = defineRoute({
124
+ render: 'ssr',
125
+ hydrate: 'visible',
126
+ offline: 'runtime',
127
+ // An org's rows are not public: a route with no policy registers \`auth: 'public'\` and its
128
+ // response carries no \`vary: cookie\`. Which ORG is the query's decision, on every read.
129
+ policy: { permission: '${feature.kebab}:read' satisfies KnownPermission },
130
+ // measured: ${RESOURCE_PAGE_MEASURED} (\`x build --target static\`, Bun 1.4.0) — the form island and the
131
+ // \`visible\` runtime. why: the form is Solid, @ultimat3/ui's Form/Input/Button and the transport.
132
+ // A raise states its own measured bytes and why here, in the same diff (\`X_BUDGET_EXCEEDED\`).
133
+ budget: { js: '${RESOURCE_PAGE_BUDGET}' },
134
+ load,
135
+ meta: ({ t }) => ({
136
+ title: t('${titleKey(path)}'),
137
+ description: t('${titleKey(path).replace('.title', '.description')}'),
138
+ }),
139
+ });
140
+
141
+ export interface PageProps {
142
+ readonly data: PageData;
143
+ }
144
+
145
+ export function Page(props: PageProps) {${options.catalogModule === undefined ? '' : '\n const t = useT();\n'}
146
+ return (
147
+ ${open}
148
+ ${inner(`<PageHeader title={t('${titleKey(path)}')} />
149
+ <Section title={t('app.${feature.kebab}.new')}>
150
+ <${feature.pascal}Form
151
+ endpoint={derivePath('create${feature.pascal}').path}
152
+ locale={currentLocale()}
153
+ currency={CURRENCY}
154
+ labels={{
155
+ title: t('app.${feature.kebab}.titleLabel'),
156
+ price: t('app.${feature.kebab}.priceLabel'),
157
+ submit: t('app.${feature.kebab}.submit'),
158
+ saved: t('app.${feature.kebab}.saved'),
159
+ retry: t('app.${feature.kebab}.retry'),
160
+ }}
161
+ >
162
+ <Skeleton lines={3} />
163
+ </${feature.pascal}Form>
164
+ </Section>
165
+ <AsyncRegion
166
+ state={props.data}
167
+ reserve={{ lines: 3 }}
168
+ empty={() => <EmptyState title={t('app.${feature.kebab}.empty')} />}
169
+ ready={(rows) => <${feature.pascal}List rows={rows} />}
170
+ />`)}
171
+ ${close}
172
+ );
173
+ }
174
+ `;
175
+ };
176
+
177
+ const ORG = '00000000-0000-4000-8000-000000000002';
178
+ const OTHER_ORG = '00000000-0000-4000-8000-000000000009';
179
+
180
+ const pageTest = (options: ResourcePageOptions): string => {
181
+ const { feature } = options;
182
+ const t = options.catalogModule === undefined ? '' : '\n const t = useT();';
183
+ return `// /${feature.pluralKebab}, rendered as a request renders it: \`load\` reads the actor's org through the
184
+ // slice's query — the in-memory driver under test — and the page shows the rows, the empty state
185
+ // or the refusal, with the form island beside them in every one.
186
+ ${sortedImports([
187
+ catalogImport(options.catalogModule),
188
+ `import { driver } from '${options.dbModule}';`,
189
+ "import { createContext, frozenClock, runWithContext } from '@ultimat3/core';",
190
+ "import { testActor } from '@ultimat3/policy';",
191
+ "import { afterEach, expect, renderRoute, unitTest } from '@ultimat3/testing';",
192
+ ])}
193
+ // The app's API, as boot loads it: \`defineApi\` is what names the query the page reads, and a
194
+ // query with no name is refused (X_QUERY_UNREGISTERED) before it reads a row.
195
+ import '../../api';
196
+ import * as repo from '../${feature.kebab}/repo';
197
+ import * as page from './page';
198
+
199
+ const url = 'https://example.test/${feature.pluralKebab}';
200
+ const orgId = '${ORG}';
201
+ const otherOrg = '${OTHER_ORG}';
202
+ // The read grant, named once so every line below fits the formatter's width at any feature name.
203
+ const read = '${feature.kebab}:read';
204
+ // Holds the read grant in the org: what a member of it is.
205
+ const member = testActor('member', { orgId, permissions: [read] }).actor;
206
+
207
+ // The request clock, so \`createdAt\` is an instant this file chose and "newest" is decidable.
208
+ const clock = frozenClock('2026-01-01T00:00:00.000Z');
209
+
210
+ const store = (title: string) => {
211
+ const draft = { orgId, title, price: { minor: 1200, currency: 'USD' } };
212
+ return runWithContext(createContext({ actor: member, clock }), () => repo.insert(draft));
213
+ };
214
+
215
+ // One store per process: without this, one test's rows are the next test's fixtures.
216
+ afterEach(() => {
217
+ driver.reset?.();
218
+ });
219
+
220
+ unitTest('the page lists the org’s rows, newest first, beside the form', async () => {
221
+ await store('older');
222
+ clock.advance(1000);
223
+ await store('newer');
224
+ const view = await renderRoute(page, { url, actor: member });
225
+ expect(view.data.status).toBe('ready');
226
+ expect(view.text.indexOf('newer')).toBeLessThan(view.text.indexOf('older'));
227
+ // The form is the one island, and it wakes when it is visible.
228
+ expect(view.islands.map((island) => island.strategy)).toEqual(['visible']);
229
+ expect(view.islands[0]?.moduleId).toContain('${feature.kebab}-form');
230
+ });
231
+
232
+ unitTest('an org with no rows reads the empty state, not an empty list', async () => {${t}
233
+ const view = await renderRoute(page, { url, actor: member });
234
+ expect(view.data).toEqual({ status: 'ready', data: [] });
235
+ expect(view.text).toContain(t('app.${feature.kebab}.empty'));
236
+ expect(view.meta.title).toBe(t('${titleKey(feature.pluralKebab)}'));
237
+ });
238
+
239
+ unitTest('a member of another org reads none of this org’s rows', async () => {
240
+ await store('ours');
241
+ // The same page, the same URL: which org it reads is the actor's, never a parameter.
242
+ const outsider = testActor('outsider', { orgId: otherOrg, permissions: [read] }).actor;
243
+ const view = await renderRoute(page, { url, actor: outsider });
244
+ expect(view.data).toEqual({ status: 'ready', data: [] });
245
+ });
246
+
247
+ unitTest('a caller the read refuses sees the refusal, and the page around it', async () => {
248
+ // In the org, without the grant: the query's policy refuses before any row is read.
249
+ const stranger = testActor('stranger', { orgId }).actor;
250
+ const view = await renderRoute(page, { url, actor: stranger });
251
+ expect(view.data.status).toBe('failed');
252
+ expect(view.html).toContain('X_FORBIDDEN');
253
+ expect(view.islands).toHaveLength(1);
254
+ });
255
+
256
+ unitTest('it is gated, rendered per request, and budgeted', () => {
257
+ expect(page.config.render).toBe('ssr');
258
+ expect(page.config.policy?.permission).toBe(read);
259
+ expect(page.config.offline).toBe('runtime');
260
+ expect(page.config.budget.js).toBe('${RESOURCE_PAGE_BUDGET}');
261
+ });
262
+ `;
263
+ };
264
+
265
+ /** The resource's `page.tsx` and its unit test. The stylesheet, e2e test and catalog are `x g route`'s. */
266
+ export const resourcePageFiles = (options: ResourcePageOptions): readonly GeneratedFile[] => [
267
+ { path: `${options.pageDir}/page.tsx`, contents: pageSource(options) },
268
+ { path: `${options.pageDir}/page.test.ts`, contents: pageTest(options) },
269
+ ];
@@ -0,0 +1,96 @@
1
+ // `x g resource`'s service module and its test: business logic over the slice's repo, covered
2
+ // against the in-memory driver. Split from `resource.ts`, which composes the slice — this file
3
+ // owns the one module in it that holds a rule.
4
+
5
+ import { sortedImports } from './imports';
6
+ import type { GeneratedFile, NameSet } from './naming';
7
+
8
+ const serviceSource = (
9
+ feature: NameSet,
10
+ ): string => `// Business logic for ${feature.pluralKebab}. Knows nothing about HTTP or requests, so a job and an
11
+ // action can both call it. Takes values, not a request.
12
+
13
+ import type { ${feature.pascal} } from './entity';
14
+ import { ${feature.pascal}NotFoundError } from './errors';
15
+ import * as repo from './repo';
16
+
17
+ /** Derived from the row, never restated: a new column reaches this input without an edit here. */
18
+ export type Create${feature.pascal}Input = Omit<${feature.pascal}, 'id' | 'createdAt'>;
19
+
20
+ /** The row, aliased once, so every signature below reads at one width whatever the feature is
21
+ * called — a generated file the app's own formatter rewrites is a red \`lint\` over code nobody
22
+ * typed. */
23
+ type Row = ${feature.pascal};
24
+
25
+ export async function create(input: Create${feature.pascal}Input): Promise<Row> {
26
+ return repo.insert(input);
27
+ }
28
+
29
+ export async function require${feature.pascal}(id: string): Promise<Row> {
30
+ const row = await repo.byId(id);
31
+ if (row === undefined) throw new ${feature.pascal}NotFoundError({ id });
32
+ return row;
33
+ }
34
+ `;
35
+
36
+ /** The feature's own code, spelled once: the class throws it and the test matches on it. */
37
+ const notFoundCode = (feature: NameSet): string =>
38
+ `X_${feature.kebab.toUpperCase().split('-').join('_')}_NOT_FOUND`;
39
+
40
+ const serviceTest = (
41
+ feature: NameSet,
42
+ dbModule: string,
43
+ ): string => `// The ${feature.kebab} service against the in-memory driver, and its failure, pinned: a code an
44
+ // agent can match on, a cause naming the row, and a fix that is an instruction.
45
+ ${sortedImports([
46
+ "import { createContext, runWithContext } from '@ultimat3/core';",
47
+ "import { testActor } from '@ultimat3/policy';",
48
+ "import { afterEach, expect, unitTest } from '@ultimat3/testing';",
49
+ `import { driver } from '${dbModule}';`,
50
+ ])}
51
+ import { ${feature.pascal}NotFoundError } from './errors';
52
+ import { create, require${feature.pascal} } from './service';
53
+
54
+ const orgId = '00000000-0000-4000-8000-000000000002';
55
+ const absent = '00000000-0000-4000-8000-0000000000ff';
56
+ const draft = { orgId, title: 'First', price: { minor: 1200, currency: 'USD' } };
57
+
58
+ /** What a request is to the repo underneath: an actor, whose org every statement runs under. */
59
+ const inOrg = <T>(run: () => Promise<T>): Promise<T> =>
60
+ runWithContext(createContext({ actor: testActor('member', { orgId }).actor }), run);
61
+
62
+ // One store per process: without this, one test's rows are the next test's fixtures.
63
+ afterEach(() => {
64
+ driver.reset?.();
65
+ });
66
+
67
+ unitTest('${feature.pascal}NotFoundError carries a code, a cause and a fix', () => {
68
+ const error = new ${feature.pascal}NotFoundError({ id: 'missing' });
69
+ expect(error).toBeUltimateError('${notFoundCode(feature)}');
70
+ expect(error.cause).toContain('missing');
71
+ expect(error.fix.length).toBeGreaterThan(0);
72
+ });
73
+
74
+ unitTest('create stores the row and the lookup answers it', async () => {
75
+ const created = await inOrg(() => create(draft));
76
+ expect(created.title).toBe('First');
77
+ // \`toEqualRow\`, never \`toEqual\`: a \`.sealed()\` column is a non-enumerable property of a row.
78
+ expect(await inOrg(() => require${feature.pascal}(created.id))).toEqualRow(created);
79
+ });
80
+
81
+ unitTest('the lookup refuses an id nothing holds', async () => {
82
+ const lookup = inOrg(() => require${feature.pascal}(absent));
83
+ const refused = await lookup.catch((error: unknown) => error);
84
+ expect(refused).toBeUltimateError('${notFoundCode(feature)}');
85
+ });
86
+ `;
87
+
88
+ /** `service.ts` and the test that covers it. `dbModule` is where the test resets the driver. */
89
+ export const serviceFiles = (
90
+ feature: NameSet,
91
+ dir: string,
92
+ dbModule: string,
93
+ ): readonly GeneratedFile[] => [
94
+ { path: `${dir}/service.ts`, contents: serviceSource(feature) },
95
+ { path: `${dir}/service.test.ts`, contents: serviceTest(feature, dbModule) },
96
+ ];