@ultimat3/cli 29.3.2 → 30.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 (448) hide show
  1. package/CLAUDE.md +64 -40
  2. package/README.md +61 -11
  3. package/package.json +42 -28
  4. package/src/affected-tests.ts +97 -0
  5. package/src/affected-unowned.ts +43 -0
  6. package/src/affected.ts +25 -6
  7. package/src/app-boundaries.ts +29 -1
  8. package/src/app-evals.ts +12 -4
  9. package/src/app-load-unresolved.ts +62 -0
  10. package/src/app-load.ts +30 -37
  11. package/src/app-mcp.ts +14 -2
  12. package/src/app-module-graph.ts +232 -0
  13. package/src/app-reload-graph.ts +2 -2
  14. package/src/app-root.ts +37 -1
  15. package/src/bin.ts +1 -0
  16. package/src/boundary-findings.ts +6 -0
  17. package/src/budgets-freshness.ts +32 -0
  18. package/src/budgets.ts +20 -1
  19. package/src/build-capture-env.ts +9 -0
  20. package/src/cdp-shot-a11y.ts +2 -39
  21. package/src/ci-tested.ts +243 -0
  22. package/src/client-emit-customer-runtime.ts +139 -0
  23. package/src/client-emit-customer.ts +108 -0
  24. package/src/client-emit-drift.ts +72 -0
  25. package/src/client-emit-header.ts +67 -0
  26. package/src/client-emit-schemas.ts +175 -0
  27. package/src/client-emit-types.ts +121 -0
  28. package/src/client-emit-vendor.ts +146 -0
  29. package/src/client-errors-routes.ts +229 -0
  30. package/src/client-errors-script.ts +74 -0
  31. package/src/client-vitals-route.ts +197 -0
  32. package/src/client-vitals-script.ts +56 -0
  33. package/src/cmd-affected.ts +23 -0
  34. package/src/cmd-build-spec.ts +7 -1
  35. package/src/cmd-build.ts +35 -9
  36. package/src/cmd-ci-spec.ts +42 -6
  37. package/src/cmd-ci.ts +99 -2
  38. package/src/cmd-client-customer.ts +134 -0
  39. package/src/cmd-client-source.ts +81 -0
  40. package/src/cmd-client-spec.ts +48 -0
  41. package/src/cmd-client-write.ts +41 -0
  42. package/src/cmd-client.ts +111 -0
  43. package/src/cmd-db-recount.ts +120 -0
  44. package/src/cmd-db-spec.ts +36 -6
  45. package/src/cmd-db.ts +13 -3
  46. package/src/cmd-deploy-compose.ts +61 -3
  47. package/src/cmd-deploy-drain.ts +10 -2
  48. package/src/cmd-deploy-helm.ts +11 -0
  49. package/src/cmd-deploy-preflight.ts +118 -0
  50. package/src/cmd-deploy-spec.ts +58 -6
  51. package/src/cmd-deploy-verify.ts +380 -0
  52. package/src/cmd-deploy.ts +42 -9
  53. package/src/cmd-docs.ts +34 -4
  54. package/src/cmd-doctor.ts +66 -9
  55. package/src/cmd-drill-spec.ts +25 -0
  56. package/src/cmd-drill.ts +176 -0
  57. package/src/cmd-env-init.ts +102 -0
  58. package/src/cmd-env-spec.ts +10 -3
  59. package/src/cmd-env.ts +15 -10
  60. package/src/cmd-errors-recent.ts +60 -0
  61. package/src/cmd-errors-spec.ts +9 -1
  62. package/src/cmd-errors.ts +4 -0
  63. package/src/cmd-exercise-spec.ts +19 -0
  64. package/src/cmd-exercise.ts +129 -0
  65. package/src/cmd-generate-spec.ts +9 -1
  66. package/src/cmd-generate.ts +32 -27
  67. package/src/cmd-graph-load.ts +97 -0
  68. package/src/cmd-graph-spec.ts +16 -0
  69. package/src/cmd-graph.ts +73 -0
  70. package/src/cmd-i18n.ts +4 -1
  71. package/src/cmd-impact-spec.ts +13 -0
  72. package/src/cmd-impact.ts +64 -0
  73. package/src/cmd-jobs-resolve.ts +76 -0
  74. package/src/cmd-jobs-spec.ts +21 -2
  75. package/src/cmd-jobs.ts +10 -6
  76. package/src/cmd-mail-spec.ts +28 -0
  77. package/src/cmd-mail.ts +70 -0
  78. package/src/cmd-mcp-lock.ts +69 -0
  79. package/src/cmd-mcp-spec.ts +23 -4
  80. package/src/cmd-mcp.ts +17 -3
  81. package/src/cmd-new-spec.ts +8 -1
  82. package/src/cmd-new.ts +61 -9
  83. package/src/cmd-planned.ts +0 -6
  84. package/src/cmd-repro-spec.ts +31 -0
  85. package/src/cmd-repro.ts +174 -0
  86. package/src/cmd-stop-spec.ts +21 -0
  87. package/src/cmd-stop.ts +166 -0
  88. package/src/cmd-test-spec.ts +17 -4
  89. package/src/cmd-test.ts +70 -14
  90. package/src/cmd-upgrade-spec.ts +26 -0
  91. package/src/cmd-upgrade.ts +274 -0
  92. package/src/cmd-verify-spec.ts +3 -3
  93. package/src/db-backfill.ts +6 -0
  94. package/src/db-diagnostics.ts +106 -0
  95. package/src/db-generate.ts +28 -8
  96. package/src/db-reset-template.ts +1 -1
  97. package/src/db-rolling.ts +45 -0
  98. package/src/db-seed-generate.ts +188 -0
  99. package/src/deploy-verify-findings.ts +98 -0
  100. package/src/deploy-verify-flags.ts +109 -0
  101. package/src/deploy-verify-grade.ts +185 -0
  102. package/src/deploy-verify-queues.ts +53 -0
  103. package/src/dev-boot.ts +35 -15
  104. package/src/dev-child-watch.ts +16 -4
  105. package/src/dev-dashboard-guard.ts +8 -1
  106. package/src/dev-dashboard.ts +7 -0
  107. package/src/dev-lock-identity.ts +61 -0
  108. package/src/dev-lock-port-holder.ts +80 -0
  109. package/src/dev-lock-steal.ts +124 -0
  110. package/src/dev-lock.ts +92 -120
  111. package/src/dev-mail-templates.ts +51 -0
  112. package/src/dev-rebuild.ts +26 -2
  113. package/src/dev-route-table.ts +17 -0
  114. package/src/dev-supervisor.ts +5 -2
  115. package/src/dev-watch-tree.ts +11 -10
  116. package/src/dev-watch.ts +1 -1
  117. package/src/dispatch.ts +29 -6
  118. package/src/docs-section.ts +69 -0
  119. package/src/docs-sources.ts +100 -0
  120. package/src/doctor-auth.ts +11 -2
  121. package/src/doctor-chart-alerts.ts +55 -0
  122. package/src/doctor-pooler.ts +107 -0
  123. package/src/doctor-rolling.ts +35 -0
  124. package/src/doctor-row-security.ts +40 -0
  125. package/src/document-options.ts +13 -0
  126. package/src/drill-sync-lane.ts +25 -0
  127. package/src/drill-targets.ts +39 -0
  128. package/src/duplicate-packages.ts +1 -1
  129. package/src/env-production.ts +93 -0
  130. package/src/error-codes-shot.ts +59 -0
  131. package/src/error-codes-tooling.ts +29 -0
  132. package/src/error-codes.ts +100 -54
  133. package/src/error-contract.ts +2 -2
  134. package/src/errors.ts +22 -3
  135. package/src/exercise-database.ts +99 -0
  136. package/src/exercise-findings.ts +159 -0
  137. package/src/exercise-overrides.ts +42 -0
  138. package/src/exercise-registration.ts +58 -0
  139. package/src/exercise-static.ts +88 -0
  140. package/src/exercise-subject.ts +69 -0
  141. package/src/failure-capture-boot.ts +30 -0
  142. package/src/framework-env.ts +7 -1
  143. package/src/framework-schema.ts +72 -5
  144. package/src/generate-catalog-locales.ts +19 -0
  145. package/src/generate-chart-alerts.ts +156 -0
  146. package/src/generate-feature-required.ts +65 -0
  147. package/src/generate-feature.ts +83 -6
  148. package/src/generate-files.ts +24 -0
  149. package/src/generate-format-written.ts +32 -0
  150. package/src/generate-kinds.ts +6 -0
  151. package/src/generate-sign-in-config.ts +100 -0
  152. package/src/generate-sign-in-manifest.ts +28 -0
  153. package/src/generate-sign-in-mode.ts +101 -0
  154. package/src/generate-sign-in.ts +250 -0
  155. package/src/generate-write.ts +44 -7
  156. package/src/hand-rolled-socket.ts +72 -0
  157. package/src/i18n-app-key-shapes.ts +29 -0
  158. package/src/i18n-audit.ts +10 -3
  159. package/src/i18n-registration.ts +18 -3
  160. package/src/index.ts +2 -0
  161. package/src/island-bundle.ts +4 -3
  162. package/src/island-disposer.ts +116 -0
  163. package/src/island-realtime-state.ts +34 -6
  164. package/src/island-realtime.ts +6 -2
  165. package/src/island-routes.ts +3 -2
  166. package/src/isr-query-advice.ts +6 -11
  167. package/src/isr-query-guard.ts +4 -3
  168. package/src/job-budget.ts +95 -0
  169. package/src/jobs-json.ts +11 -0
  170. package/src/lint-findings.ts +104 -0
  171. package/src/local-cli.ts +34 -4
  172. package/src/loopback-host.ts +2 -2
  173. package/src/mail-preview-findings.ts +24 -0
  174. package/src/mcp-errors.ts +82 -2
  175. package/src/mcp-host.ts +11 -1
  176. package/src/mcp-lock-findings.ts +57 -0
  177. package/src/measure-paths.ts +12 -4
  178. package/src/measure-scope.ts +27 -13
  179. package/src/messages-client.ts +23 -0
  180. package/src/messages-db.ts +56 -0
  181. package/src/messages-gate.ts +107 -0
  182. package/src/messages-repro.ts +11 -0
  183. package/src/messages-shot.ts +51 -0
  184. package/src/messages-upgrade.ts +45 -0
  185. package/src/messages.ts +53 -178
  186. package/src/metrics-endpoint.ts +23 -5
  187. package/src/new-shapes-generate.ts +71 -0
  188. package/src/new-shapes.ts +63 -0
  189. package/src/openapi-compile-ir.ts +68 -0
  190. package/src/openapi-compile-schema.ts +284 -0
  191. package/src/openapi-compile.ts +273 -0
  192. package/src/openapi-refuse.ts +45 -0
  193. package/src/otlp-export.ts +2 -1
  194. package/src/output-findings.ts +123 -0
  195. package/src/output.ts +32 -8
  196. package/src/package-boundaries.ts +110 -0
  197. package/src/page-pins.ts +23 -0
  198. package/src/plain-routes.ts +13 -0
  199. package/src/prerender-entries.ts +59 -0
  200. package/src/prerender-heaviest.ts +27 -0
  201. package/src/prerender-preload.ts +6 -0
  202. package/src/prerender-report.ts +12 -0
  203. package/src/prerender-seo-reads.ts +38 -0
  204. package/src/prerender-served.ts +22 -7
  205. package/src/prerender-vitals.ts +42 -0
  206. package/src/prerender.ts +61 -32
  207. package/src/pwa-artifacts.ts +4 -3
  208. package/src/pwa-manifest-locales.ts +1 -1
  209. package/src/registry.ts +18 -0
  210. package/src/repro-bundle.ts +65 -0
  211. package/src/repro-errors.ts +47 -0
  212. package/src/repro-job.ts +72 -0
  213. package/src/repro-literal.ts +41 -0
  214. package/src/repro-request.ts +48 -0
  215. package/src/repro-write.ts +100 -0
  216. package/src/retain-sweep.ts +27 -0
  217. package/src/retained-assets-boot.ts +113 -0
  218. package/src/retained-assets-layout.ts +110 -0
  219. package/src/retained-assets-read.ts +142 -0
  220. package/src/retained-assets-sweep.ts +70 -0
  221. package/src/retained-assets.ts +171 -0
  222. package/src/revalidated-response.ts +8 -5
  223. package/src/role-replicator.ts +7 -1
  224. package/src/role-start-devices.ts +119 -0
  225. package/src/role-start-types.ts +6 -0
  226. package/src/role-start.ts +22 -5
  227. package/src/role-sync.ts +7 -1
  228. package/src/route-islands.ts +42 -0
  229. package/src/route-table.ts +2 -1
  230. package/src/row-security-boot.ts +58 -0
  231. package/src/row-security-wanted.ts +32 -0
  232. package/src/runtime-admin.ts +0 -1
  233. package/src/runtime-assets-site-cache.ts +55 -0
  234. package/src/runtime-assets-site.ts +136 -0
  235. package/src/runtime-assets.ts +99 -55
  236. package/src/runtime-bindings.ts +19 -3
  237. package/src/runtime-cache.ts +23 -14
  238. package/src/runtime-isr-outcome.ts +114 -60
  239. package/src/runtime-purge.ts +14 -2
  240. package/src/runtime-queue.ts +31 -4
  241. package/src/runtime-render-document.ts +204 -0
  242. package/src/runtime-render.ts +48 -198
  243. package/src/runtime-replica-position.ts +62 -0
  244. package/src/runtime-replica.ts +18 -3
  245. package/src/runtime-s3-path-style.ts +17 -0
  246. package/src/runtime-services.ts +19 -6
  247. package/src/runtime-statement-bound.ts +22 -0
  248. package/src/runtime-storage-upload.ts +9 -0
  249. package/src/runtime-storage.ts +42 -138
  250. package/src/schema-diff-views.ts +54 -0
  251. package/src/schema-diff.ts +21 -3
  252. package/src/schema-drift.ts +28 -9
  253. package/src/schema-dump-drift.ts +91 -7
  254. package/src/script-csp.ts +1 -1
  255. package/src/seo-cards.ts +26 -0
  256. package/src/seo-routes.ts +22 -8
  257. package/src/serve-boot.ts +54 -21
  258. package/src/serve-drain.ts +1 -0
  259. package/src/serve-env.ts +26 -8
  260. package/src/serve-retire.ts +12 -6
  261. package/src/serve-web.ts +42 -4
  262. package/src/serve.ts +9 -7
  263. package/src/site-asset-routes.ts +17 -27
  264. package/src/site-config.ts +4 -0
  265. package/src/site-llms-pages.ts +106 -0
  266. package/src/site-llms.ts +64 -0
  267. package/src/site-seo-write.ts +4 -2
  268. package/src/site-seo.ts +55 -6
  269. package/src/source-mtime.ts +50 -0
  270. package/src/static-capture-errors.ts +41 -0
  271. package/src/static-capture.ts +56 -0
  272. package/src/static-cards-serve.ts +39 -0
  273. package/src/static-cards.ts +93 -0
  274. package/src/static-document.ts +14 -13
  275. package/src/static-images.ts +115 -0
  276. package/src/stream-hole-scope.ts +55 -0
  277. package/src/style-bundle.ts +1 -1
  278. package/src/style-routes.ts +3 -2
  279. package/src/sw-precache-plan.ts +2 -2
  280. package/src/sw-routes.ts +2 -2
  281. package/src/templates/action-tests.ts +306 -0
  282. package/src/templates/action.ts +81 -269
  283. package/src/templates/backfill.ts +1 -1
  284. package/src/templates/entity.ts +21 -5
  285. package/src/templates/github/ci.yml.ts +8 -9
  286. package/src/templates/guard-bare-error.ts +2 -2
  287. package/src/templates/guard-error-declared.ts +263 -0
  288. package/src/templates/guard-native-confirm.ts +132 -0
  289. package/src/templates/guard-raw-fetch.ts +151 -0
  290. package/src/templates/guard-raw-interval.ts +121 -0
  291. package/src/templates/guard-repo-raw-sql.ts +71 -2
  292. package/src/templates/job.ts +1 -1
  293. package/src/templates/naming.ts +5 -0
  294. package/src/templates/pinned/docker-compose.yml +6 -0
  295. package/src/templates/pinned-images.ts +8 -0
  296. package/src/templates/policy.ts +85 -2
  297. package/src/templates/query.ts +70 -13
  298. package/src/templates/resource-create.ts +3 -1
  299. package/src/templates/resource-form-island.ts +15 -4
  300. package/src/templates/resource-page-db.ts +83 -0
  301. package/src/templates/resource-page.ts +161 -52
  302. package/src/templates/route.ts +4 -6
  303. package/src/templates/scaffold-app.ts +3 -0
  304. package/src/templates/scaffold-auth.ts +2 -1
  305. package/src/templates/scaffold-bin.ts +5 -52
  306. package/src/templates/scaffold-claude-agents.ts +19 -19
  307. package/src/templates/scaffold-claude-commands.ts +28 -31
  308. package/src/templates/scaffold-claude.ts +26 -23
  309. package/src/templates/scaffold-container-compose.ts +6 -1
  310. package/src/templates/scaffold-container.ts +24 -4
  311. package/src/templates/scaffold-dashboard-bare.ts +3 -3
  312. package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
  313. package/src/templates/scaffold-dashboard-example-view.ts +156 -0
  314. package/src/templates/scaffold-dashboard-example.ts +43 -131
  315. package/src/templates/scaffold-dashboard-shared.ts +3 -7
  316. package/src/templates/scaffold-db-client.ts +20 -6
  317. package/src/templates/scaffold-db-package.ts +5 -14
  318. package/src/templates/scaffold-docs.ts +73 -57
  319. package/src/templates/scaffold-domain-package.ts +18 -14
  320. package/src/templates/scaffold-entries.ts +7 -2
  321. package/src/templates/scaffold-exercise.ts +37 -0
  322. package/src/templates/scaffold-guards.ts +13 -1
  323. package/src/templates/scaffold-helm-alerts.ts +234 -0
  324. package/src/templates/scaffold-helm-templates.ts +7 -5
  325. package/src/templates/scaffold-helm.ts +46 -12
  326. package/src/templates/scaffold-i18n.ts +6 -2
  327. package/src/templates/scaffold-mcp-endpoint.ts +182 -0
  328. package/src/templates/scaffold-mcp-json.ts +31 -0
  329. package/src/templates/scaffold-mcp-package.ts +59 -56
  330. package/src/templates/scaffold-repo.ts +7 -2
  331. package/src/templates/scaffold-shell.ts +45 -79
  332. package/src/templates/scaffold-test-services.ts +13 -5
  333. package/src/templates/shapes/api-keys.ts +170 -0
  334. package/src/templates/shapes/api.ts +101 -0
  335. package/src/templates/shapes/blog-site.ts +340 -0
  336. package/src/templates/shapes/blog-slice.ts +265 -0
  337. package/src/templates/shapes/blog.ts +114 -0
  338. package/src/templates/shapes/dashboard-detail.ts +242 -0
  339. package/src/templates/shapes/dashboard.ts +53 -0
  340. package/src/templates/shapes/index.ts +17 -0
  341. package/src/templates/shapes/overlay.ts +99 -0
  342. package/src/templates/shapes/shared-edits.ts +46 -0
  343. package/src/templates/sign-in-action-tests.ts +237 -0
  344. package/src/templates/sign-in-authenticator.ts +112 -0
  345. package/src/templates/sign-in-oidc.ts +189 -0
  346. package/src/templates/sign-in-pages.ts +148 -0
  347. package/src/templates/sign-in-password.ts +216 -0
  348. package/src/templates/sign-in-shared.ts +268 -0
  349. package/src/templates/slice-foundation.ts +70 -15
  350. package/src/test-batches.ts +1 -1
  351. package/src/test-database.ts +1 -1
  352. package/src/test-shards.ts +7 -1
  353. package/src/test-workers.ts +18 -12
  354. package/src/tool-gate-names.ts +34 -0
  355. package/src/upgrade-agents-link.ts +49 -0
  356. package/src/upgrade-bun-floor.ts +225 -0
  357. package/src/upgrade-gate.ts +228 -0
  358. package/src/upgrade-pins.ts +63 -0
  359. package/src/upgrade-plan.ts +214 -0
  360. package/src/upgrade-registry.ts +53 -0
  361. package/src/upgrade-report.ts +139 -0
  362. package/src/upgrade-root.ts +74 -0
  363. package/src/verify-budgets-build.ts +118 -0
  364. package/src/verify-checks.ts +67 -24
  365. package/src/verify-coverage-run.ts +1 -1
  366. package/src/verify-e2e-lane.ts +41 -0
  367. package/src/verify-exercise.ts +64 -0
  368. package/src/verify-floor.ts +41 -0
  369. package/src/verify-merge.ts +19 -4
  370. package/src/verify-role-load.ts +2 -0
  371. package/src/verify-run.ts +28 -8
  372. package/src/verify-shard.ts +12 -2
  373. package/src/verify-skip-ceiling.ts +87 -0
  374. package/src/verify-step.ts +5 -0
  375. package/src/verify-tests.ts +25 -5
  376. package/src/view-drift.ts +55 -0
  377. package/src/worker-bundle.ts +3 -2
  378. package/src/workspace-graph.ts +38 -19
  379. package/src/write-generated.ts +43 -0
  380. package/src/zone-boot.ts +20 -0
  381. package/wiki/Actions.md +342 -0
  382. package/wiki/Admin-Dashboard.md +432 -0
  383. package/wiki/Agents.md +958 -0
  384. package/wiki/Auth.md +195 -0
  385. package/wiki/Bare-VM.md +111 -0
  386. package/wiki/Batching-And-Preloading.md +136 -0
  387. package/wiki/Building-Your-Own-Base.md +203 -0
  388. package/wiki/CI-Parallel-Gate.md +220 -0
  389. package/wiki/CLI-Reference.md +1894 -0
  390. package/wiki/Caching-And-Invalidation.md +255 -0
  391. package/wiki/Client-Data.md +194 -0
  392. package/wiki/Client-Navigation.md +513 -0
  393. package/wiki/Compared.md +93 -0
  394. package/wiki/Configuration.md +868 -0
  395. package/wiki/Contributing.md +12 -0
  396. package/wiki/Deploy-Recipes.md +122 -0
  397. package/wiki/Deployment.md +508 -0
  398. package/wiki/Entities-And-Migrations.md +1116 -0
  399. package/wiki/FAQ.md +175 -0
  400. package/wiki/Feature-Flags.md +65 -0
  401. package/wiki/Getting-Started.md +221 -0
  402. package/wiki/Home.md +160 -0
  403. package/wiki/I18n.md +239 -0
  404. package/wiki/Installation.md +206 -0
  405. package/wiki/Interface-Rules.md +339 -0
  406. package/wiki/Jobs-And-Workflows.md +783 -0
  407. package/wiki/Known-Gaps.md +185 -0
  408. package/wiki/MCP-And-AI.md +753 -0
  409. package/wiki/Mail.md +138 -0
  410. package/wiki/Migrating-An-Existing-App.md +594 -0
  411. package/wiki/Migrating-From-Django.md +54 -0
  412. package/wiki/Migrating-From-Laravel.md +53 -0
  413. package/wiki/Migrating-From-Node-Drizzle.md +53 -0
  414. package/wiki/Migrating-From-Rails.md +53 -0
  415. package/wiki/Migrations-And-Backfills.md +552 -0
  416. package/wiki/Money.md +338 -0
  417. package/wiki/N-Plus-One-Detection.md +197 -0
  418. package/wiki/Notify.md +208 -0
  419. package/wiki/Observability.md +245 -0
  420. package/wiki/Outbound-HTTP.md +109 -0
  421. package/wiki/PWA-And-Offline.md +343 -0
  422. package/wiki/Policies-And-Authz.md +251 -0
  423. package/wiki/Project-Layout.md +150 -0
  424. package/wiki/Queries-And-Live-Queries.md +429 -0
  425. package/wiki/Realtime.md +734 -0
  426. package/wiki/Resource-Management.md +121 -0
  427. package/wiki/Routes-And-Render-Modes.md +521 -0
  428. package/wiki/SEO.md +288 -0
  429. package/wiki/Scheduled-Tasks.md +175 -0
  430. package/wiki/Scraping.md +493 -0
  431. package/wiki/Shapes.md +63 -0
  432. package/wiki/Stability.md +97 -0
  433. package/wiki/Static-Assets.md +166 -0
  434. package/wiki/Storage-And-Uploads.md +404 -0
  435. package/wiki/Testing.md +1313 -0
  436. package/wiki/The-Eight-Primitives.md +247 -0
  437. package/wiki/Theming.md +362 -0
  438. package/wiki/Timezones-And-Dates.md +194 -0
  439. package/wiki/Troubleshooting.md +163 -0
  440. package/wiki/Tutorial-01-First-App.md +289 -0
  441. package/wiki/Tutorial-02-First-Feature.md +339 -0
  442. package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
  443. package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
  444. package/wiki/Tutorial-05-Deploy-Free.md +231 -0
  445. package/wiki/Tutorial-06-Growing-Up.md +158 -0
  446. package/wiki/UI-Components.md +156 -0
  447. package/wiki/Upgrading.md +3667 -0
  448. package/src/stored-object-headers.ts +0 -43
@@ -16,8 +16,10 @@ const source =
16
16
  // step — nothing registers this file, so nothing can forget to. Delete it to drop the rule.
17
17
  //
18
18
  // Only a statement the handle can express is reported: \`select *\`, \`insert\`, \`update\` or \`delete\`
19
- // on ONE table, with plain comparisons. A join, an aggregate, a CTE, a window, an \`or\`, a function
19
+ // on ONE table, with plain comparisons, and \`select * … where id = ? for update\`. A join, an aggregate, a CTE, a window, an \`or\`, a function
20
20
  // call and a statement composed from another fragment are past what the handle says, and silent.
21
+ // One more shape is reported in any statement, join or not: an \`order by date_trunc('milliseconds', …)\`,
22
+ // whose repair is \`keyset()\` from \`@ultimat3/db\`.
21
23
 
22
24
  import type { Finding, Guard } from '@ultimat3/cli';
23
25
 
@@ -142,6 +144,8 @@ const UPDATE = new RegExp(
142
144
  \`^update \${TABLE} set \${ASSIGNMENT}(?:, ?\${ASSIGNMENT})* where \${WHERE}(?: returning \\\\*)?$\`,
143
145
  );
144
146
  const DELETE = new RegExp(\`^delete from \${TABLE} where \${WHERE}(?: returning \\\\*)?$\`);
147
+ /** One row by its id, locked: the read-modify-write \`lock\` exists for. Another lock mode is silent. */
148
+ const LOCKED = new RegExp(\`^select \\\\* from \${TABLE} where \${WHERE}(?: limit 1)? for update$\`);
145
149
 
146
150
  /** \`short_links\` → \`shortLinks\`: the handle is keyed by the camelCase the entity declares. */
147
151
  const camel = (name: string): string =>
@@ -190,6 +194,10 @@ export function handleCallFor(sql: string): string | undefined {
190
194
  .trim()
191
195
  .replace(/;$/, '')
192
196
  .trim();
197
+ const locked = LOCKED.exec(text);
198
+ if (locked !== null && /(^| )"?id"? = \\?/.test(locked[2] ?? '')) {
199
+ return \`db.\${camel(locked[1] ?? '')}.lock(id, fn)\`;
200
+ }
193
201
  const select = SELECT.exec(text);
194
202
  if (select !== null) {
195
203
  const [, table = '', where, column, direction, limit] = select;
@@ -220,11 +228,38 @@ export function handleCallFor(sql: string): string | undefined {
220
228
  return undefined;
221
229
  }
222
230
 
231
+ /**
232
+ * \`order by date_trunc('milliseconds', col)\`: the cure for a JS \`Date\` dropping microseconds, and
233
+ * the plan's loss — an expression the index does not hold sorts every row. A line comment inside
234
+ * the statement is not code, so it is dropped before the match.
235
+ */
236
+ export function truncatedOrder(sql: string): boolean {
237
+ const text = sql
238
+ .replaceAll(/--[^\\n]*/g, ' ')
239
+ .toLowerCase()
240
+ .replaceAll(/\\s+/g, ' ');
241
+ return /\\border by [^;]*\\bdate_trunc ?\\( ?'milliseconds'/.test(text);
242
+ }
243
+
244
+ const KEYSET_CALL =
245
+ "keyset({ scope, keys: [{ column: raw('p.created_at'), kind: 'timestamptz', direction: 'desc' }, { column: raw('p.id'), kind: 'uuid', direction: 'desc' }], after, limit })";
246
+
247
+ const truncatedFinding = (at: string): Finding => ({
248
+ code: CODE,
249
+ cause: \`\${at} orders by date_trunc('milliseconds', …), the workaround for a Date losing microseconds — and the index cannot serve that expression: measured on 300,000 rows, an Index Only Scan (0.2 ms) became a Parallel Seq Scan and a sort (100 ms)\`,
250
+ fix: \`\${KEYSET_CALL} — from @ultimat3/db, its select, where, orderBy and limit spliced in place of the date_trunc order at \${at}, then: x verify\`,
251
+ at,
252
+ });
253
+
223
254
  /** Pure — the caller does the I/O — so the rule is testable without a filesystem. */
224
255
  export function rawRepoSql(files: readonly RepoFile[]): readonly Finding[] {
225
256
  const findings: Finding[] = [];
226
257
  for (const file of files) {
227
258
  for (const statement of sqlStatements(file.source)) {
259
+ if (truncatedOrder(statement.text)) {
260
+ findings.push(truncatedFinding(\`\${file.path}:\${statement.line}\`));
261
+ continue;
262
+ }
228
263
  const call = handleCallFor(statement.text);
229
264
  if (call === undefined) continue;
230
265
  const at = \`\${file.path}:\${statement.line}\`;
@@ -254,7 +289,7 @@ const test =
254
289
  // a green gate over the convention it was written to enforce.
255
290
 
256
291
  import { expect, unitTest } from '@ultimat3/testing';
257
- import { handleCallFor, rawRepoSql, sqlStatements } from './repo-raw-sql';
292
+ import { handleCallFor, rawRepoSql, sqlStatements, truncatedOrder } from './repo-raw-sql';
258
293
 
259
294
  const REPO = 'apps/web/app/post/repo.ts';
260
295
  const repo = (source: string) => [{ path: REPO, source }];
@@ -302,6 +337,18 @@ unitTest('insert, update and delete on one table are the same rule', () => {
302
337
  );
303
338
  });
304
339
 
340
+ unitTest('a row locked by its id is the lock call, with or without its tenant', () => {
341
+ const locks = [
342
+ 'select * from posts where id = ? for update',
343
+ 'select * from posts where id = ? limit 1 for update',
344
+ 'SELECT * FROM posts WHERE org_id = ? AND id = ? FOR UPDATE;',
345
+ ];
346
+ for (const sql of locks) expect(handleCallFor(sql)).toBe('db.posts.lock(id, fn)');
347
+ const source = \`const row = sql\\\`select * from short_links where id = \${hole('id')} for update\\\`;\`;
348
+ const findings = rawRepoSql(repo(source));
349
+ expect(findings[0]?.fix).toStartWith('db.shortLinks.lock(id, fn) — ');
350
+ });
351
+
305
352
  // The legitimate lookalikes — the reason the rule can stay switched on. Each is a statement the
306
353
  // handle has no word for, and raw SQL in a repo is the right answer for it.
307
354
  unitTest('a statement past what the handle says is silent', () => {
@@ -314,10 +361,32 @@ unitTest('a statement past what the handle says is silent', () => {
314
361
  'with recent as (select * from posts) select * from recent',
315
362
  'update posts set like_count = like_count + 1 where id = ?',
316
363
  'insert into likes (post_id, user_id) values (?, ?) on conflict do nothing',
364
+ 'select * from jobs where state = ? for update skip locked',
365
+ 'select * from posts where id = ? for no key update',
366
+ 'select * from posts where org_id = ? for update',
317
367
  ];
318
368
  for (const sql of silent) expect(handleCallFor(sql)).toBeUndefined();
319
369
  });
320
370
 
371
+ unitTest(
372
+ 'an order by a millisecond-truncated column is refused in any statement, naming keyset()',
373
+ () => {
374
+ const join =
375
+ "select p.* from posts p join members m on m.id = p.author_id order by date_trunc('milliseconds', p.created_at) desc, p.id desc";
376
+ expect(truncatedOrder(join)).toBe(true);
377
+ expect(
378
+ truncatedOrder(
379
+ "select * from posts where date_trunc('day', created_at) = ? order by created_at",
380
+ ),
381
+ ).toBe(false);
382
+ expect(
383
+ truncatedOrder("select * from posts -- order by date_trunc('milliseconds', created_at)"),
384
+ ).toBe(false);
385
+ const findings = rawRepoSql(repo(\`const q = sql\\\`\${join}\\\`;\`));
386
+ expect(findings[0]?.fix).toStartWith('keyset({ scope, keys: [');
387
+ },
388
+ );
389
+
321
390
  unitTest('a statement composed from another fragment is silent', () => {
322
391
  const inner = \`sql\\\`and a = \${hole('a')}\\\`\`;
323
392
  const source = \`const rows = sql\\\`select * from posts where id = \${hole('id')} \${hole(inner)}\\\`;\`;
@@ -34,7 +34,7 @@ export const ${name.camel} = job({
34
34
  retry: { attempts: 5, backoff: 'exponential' },
35
35
  async run({ input, step }) {
36
36
  const row = await step.run('load', () => repo.byId(input.id));
37
- if (row === undefined) return { skipped: true };
37
+ if (row === null) return { skipped: true };
38
38
  await step.run('process', async () => {
39
39
  await repo.list(1);
40
40
  });
@@ -25,6 +25,11 @@ export interface GeneratedSourceFile {
25
25
  readonly path: string;
26
26
  readonly contents: string | Uint8Array;
27
27
  readonly merge?: undefined;
28
+ /**
29
+ * A sibling path this file is a RELATIVE symlink to (`AGENTS.md` → `CLAUDE.md`). `contents` is
30
+ * then the target's own text: the copy `write-generated.ts` writes where the OS refuses a link.
31
+ */
32
+ readonly link?: string;
28
33
  }
29
34
 
30
35
  /**
@@ -0,0 +1,6 @@
1
+ # The image pins a scaffolded app's compose files read (pinned-images.ts). YAML because Dependabot
2
+ # rewrites only compose files and Dockerfiles: a pin held in a template string is one it never sees.
3
+ # It is grouped in .github/dependabot.yml with docker/, so one PR moves every copy together.
4
+ services:
5
+ s3:
6
+ image: versity/versitygw:v1.9.0@sha256:eb7f786fcd91e72194e48fcf1ac16302d686bf8e3a71434bdbc067a6efd3fdc7
@@ -0,0 +1,8 @@
1
+ // The one source of a scaffolded app's Dependabot-maintained image pins: ./pinned/docker-compose.yml.
2
+
3
+ import pinned from './pinned/docker-compose.yml' with { type: 'yaml' };
4
+
5
+ const { services } = pinned as { services: { s3: { image: string } } };
6
+
7
+ /** The S3 gateway every scaffolded compose file runs. */
8
+ export const S3_GATEWAY_IMAGE = services.s3.image;
@@ -80,6 +80,82 @@ export const can${feature.pascal}Create = can(
80
80
  );
81
81
  `;
82
82
 
83
+ /** One grant and no predicate: what decides on an org has nothing to decide on in this slice. */
84
+ const grantRule = (feature: NameSet, rule: string, verb: string): string =>
85
+ wrapList(
86
+ '',
87
+ `export const can${feature.pascal}${rule} = can(`,
88
+ [`'${feature.kebab}:${verb}'`],
89
+ ');',
90
+ );
91
+
92
+ /**
93
+ * The slice's entity names no tenant (`sliceHasNoTenant`), so there is no org for a rule to compare
94
+ * and none an orgless actor lacks: the grant is the whole rule. A row-level one is the author's.
95
+ */
96
+ const singleTenantPolicySource = (
97
+ feature: NameSet,
98
+ ): string => `// Authz for the ${feature.kebab} feature, whose entity names no tenant: the grant is the rule. Every
99
+ // branch here is reachable from every surface, and predicates are synchronous on purpose.
100
+
101
+ import { tag } from '@ultimat3/cache';
102
+ import { can, definePermissions } from '@ultimat3/policy';
103
+
104
+ // The permission set, declared rather than assumed. The augmentation narrows \`can()\` to these
105
+ // strings, so a typo is a build error instead of a rule that silently never matches.
106
+ declare module '@ultimat3/policy' {
107
+ interface PermissionRegistry {
108
+ '${feature.kebab}:read': true;
109
+ '${feature.kebab}:write': true;
110
+ }
111
+ }
112
+
113
+ ${permissionSet(feature)}
114
+
115
+ /** What a write invalidates and a read depends on — one tag, both directions. */
116
+ export const ${feature.camel}Tag = tag('${feature.kebab}');
117
+
118
+ ${grantRule(feature, 'Read', 'read')}
119
+
120
+ ${grantRule(feature, 'Write', 'write')}
121
+
122
+ ${grantRule(feature, 'Create', 'write')}
123
+ `;
124
+
125
+ const singleTenantPolicyTest = (
126
+ feature: NameSet,
127
+ ): string => `// The ${feature.kebab} rules, from the DENIAL side: anonymous, and the actor holding only read.
128
+ import { testActor } from '@ultimat3/policy';
129
+ import { expect, unitTest } from '@ultimat3/testing';
130
+ ${wrapImport([`can${feature.pascal}Create`, `can${feature.pascal}Read`, `can${feature.pascal}Write`], './policy')}
131
+
132
+ // Each grant named once, so every assertion below fits the formatter's width at any feature name.
133
+ const read = '${feature.kebab}:read';
134
+ const write = '${feature.kebab}:write';
135
+ const input = {};
136
+
137
+ const reader = testActor('reader', { permissions: [read] }).actor;
138
+ const writer = testActor('writer', { permissions: [read, write] }).actor;
139
+
140
+ unitTest('${feature.camel} read denies anonymous', async () => {
141
+ await expect(can${feature.pascal}Read).toDenyPolicy({ actor: null, input });
142
+ await expect(can${feature.pascal}Read).not.toDenyPolicy({ actor: reader, input });
143
+ });
144
+
145
+ unitTest('${feature.camel} write and create deny the read-only actor', async () => {
146
+ await expect(can${feature.pascal}Write).toDenyPolicy({ actor: reader, input });
147
+ await expect(can${feature.pascal}Write).not.toDenyPolicy({ actor: writer, input });
148
+ await expect(can${feature.pascal}Create).toDenyPolicy({ actor: reader, input });
149
+ await expect(can${feature.pascal}Create).not.toDenyPolicy({ actor: writer, input });
150
+ });
151
+
152
+ unitTest('${feature.camel} rules name the permission they require', () => {
153
+ expect(can${feature.pascal}Read.permissions).toEqual(['${feature.kebab}:read']);
154
+ expect(can${feature.pascal}Write.permissions).toEqual(['${feature.kebab}:write']);
155
+ expect(can${feature.pascal}Create.permissions).toEqual(['${feature.kebab}:write']);
156
+ });
157
+ `;
158
+
83
159
  const policyTest = (
84
160
  feature: NameSet,
85
161
  ): string => `// The ${feature.kebab} rules, from the DENIAL side: anonymous, cross-org, and the actor holding
@@ -142,11 +218,18 @@ unitTest('${feature.camel} rules name the permission they require', () => {
142
218
  export function policyFiles(
143
219
  rawName: string,
144
220
  target: { readonly surfaceDir: string; readonly feature: string },
221
+ shape: { readonly singleTenant?: boolean } = {},
145
222
  ): readonly GeneratedFile[] {
146
223
  const feature = names(rawName.length > 0 ? rawName : target.feature);
147
224
  const dir = `${target.surfaceDir}/${target.feature}`;
148
225
  return [
149
- { path: `${dir}/policy.ts`, contents: policySource(feature) },
150
- { path: `${dir}/policy.test.ts`, contents: policyTest(feature) },
226
+ {
227
+ path: `${dir}/policy.ts`,
228
+ contents: shape.singleTenant ? singleTenantPolicySource(feature) : policySource(feature),
229
+ },
230
+ {
231
+ path: `${dir}/policy.test.ts`,
232
+ contents: shape.singleTenant ? singleTenantPolicyTest(feature) : policyTest(feature),
233
+ },
151
234
  ];
152
235
  }
@@ -1,13 +1,14 @@
1
- // `x g query <name> [--live]` — a read. Live queries must be deterministic and bounded, so the
1
+ // `x g query <name> --feature <slice> [--live]` — a read. Live queries must be deterministic and bounded, so the
2
2
  // generated declaration always carries `orderBy` + `limit` and the generated test pins them:
3
3
  // an unbounded live query is a memory leak that only shows up under load.
4
4
 
5
+ import { UltimateError } from '@ultimat3/core';
5
6
  import type { FeatureTarget } from './entity';
6
7
  import { sortedImports } from './imports';
7
8
  import type { GeneratedFile, NameSet } from './naming';
8
9
  import { names } from './naming';
9
10
  import { PLACEHOLDER_DB_MODULE } from './scaffold-db-client';
10
- import { sliceFoundation, sliceTakesScaffoldRow } from './slice-foundation';
11
+ import { foreignSliceEntity, sliceFoundation, sliceTakesScaffoldRow } from './slice-foundation';
11
12
  import { wrapImport } from './wrap';
12
13
 
13
14
  /** A live read always fans out fresh, so a TTL on it would only ever be dead configuration. */
@@ -30,27 +31,38 @@ ${wrapImport(
30
31
  )}
31
32
  import * as repo from '../repo';
32
33
 
34
+ /** The columns this read sorts by: \`repo.ListSort\`, and what a page's \`listUrl\` offers. */
35
+ export const ${name.camel}Sorts = ['createdAt', 'title'] as const;
36
+
33
37
  // No \`orgId\` in the input, and none in the policy: \`repo.list\` reads through the typed handle,
34
38
  // which scopes every read to the ACTOR's org, and a live window is keyed by the subscriber's
35
39
  // tenant. One source of tenancy — a parameter would be a second one a caller could set.
36
40
  export const ${name.camel} = query({
37
- input: t.object({ limit: t.number.default(50) }),
41
+ input: t.object({
42
+ limit: t.number.default(50),
43
+ // A closed set, so a sort is a column this read has an order for, never a caller's string.
44
+ sort: t.enum(${name.camel}Sorts).default('createdAt'),
45
+ dir: t.enum(['asc', 'desc']).default('desc'),
46
+ }),
38
47
  policy: can${feature.pascal}Read,
39
48
  live: ${String(live)},${cacheLine(feature, live)}
49
+ // The refusals a caller may branch on, closed: add each code this read comes to throw.
50
+ errors: [],
40
51
  // Opt-in, unlike an action's tool: a read hands rows to an agent, so silence exposes nothing.
41
52
  mcp: { expose: true, description: '${name.raw} — edit this description' },
42
- sql: ({ limit }) =>
53
+ sql: ({ limit, sort, dir }) =>
43
54
  // \`feature.table\`, not the kebab plural: \`from()\` quotes the identifier into the SQL text,
44
55
  // and the entity created the table as snake_case.
45
- from<${feature.pascal}>('${feature.table}', () => repo.list(limit))
46
- // Newest first — \`repo.list\`'s own order. These two lines are ONE read spelled twice: the
47
- // thunk is what the in-memory driver answers and this chain is the SQL Postgres runs, so a
48
- // direction that differs here is a bounded page holding different rows on each.
49
- .orderBy('createdAt', 'desc')
50
- // The primary key last is what makes the order TOTAL: \`createdAt\` alone ties, and two
51
- // rows that tie can swap between evaluations — a bounded read then drops one and repeats
52
- // the other, and a live subscription patches a row it never sent.
53
- .orderBy('id')
56
+ from<${feature.pascal}>('${feature.table}', () => repo.list(limit, sort, dir))
57
+ // \`repo.list\`'s own order. These lines are ONE read spelled twice: the thunk is what the
58
+ // in-memory driver answers and this chain is the SQL Postgres runs, so a sort or a direction
59
+ // that differs here is a bounded page holding different rows on each.
60
+ .orderBy(sort, dir)
61
+ // The primary key last, in the same direction the handle adds it, is what makes the order
62
+ // TOTAL: a sort column alone ties, and two rows that tie can swap between evaluations — a
63
+ // bounded read then drops one and repeats the other, and a live subscription patches a row
64
+ // it never sent.
65
+ .orderBy('id', dir)
54
66
  .limit(limit),
55
67
  });
56
68
  `;
@@ -121,6 +133,23 @@ ${wrapper}('${name.camel} is bounded and TOTALLY ordered', async () => {
121
133
  expect(text.slice(text.lastIndexOf('order by'))).toContain('id');
122
134
  });
123
135
 
136
+ ${wrapper}('the read sorts by the column it is asked for, either way', async () => {
137
+ // The same sort the in-memory thunk applies (\`repo.list(limit, sort, dir)\`), spelled as SQL —
138
+ // and the id tiebreak follows the direction, as the handle's does.
139
+ const reason = 'a scaffolded test asserts the SQL text; the policy is asserted separately';
140
+ const sqlFor = async (dir: 'asc' | 'desc') => {
141
+ const input = { limit: 50, sort: 'title' as const, dir };
142
+ const source = await sourceFor(target, input, { actor: null, unenforced: reason });
143
+ return source.toSQL().sql.toLowerCase();
144
+ };
145
+ const asc = await sqlFor('asc');
146
+ expect(asc).toContain('order by "title" asc');
147
+ expect(asc).toContain('"id" asc');
148
+ const desc = await sqlFor('desc');
149
+ expect(desc).toContain('order by "title" desc');
150
+ expect(desc).toContain('"id" desc');
151
+ });
152
+
124
153
  ${wrapper}('${name.camel} denies an orgless actor before a read', async () => {
125
154
  // \`.as()\` is the one read path with the actor swapped: validate, authorize, then read. The
126
155
  // denial lands before any SQL executes, which is why this needs no database.
@@ -249,6 +278,15 @@ unitTest('stored rows read back newest first, bounded, and never another org', a
249
278
  const theirs = await target.as(outsider, { limit: 50 });
250
279
  expect(theirs.map((row) => row.title)).toEqual(['theirs']);
251
280
  });
281
+
282
+ unitTest('the read is one statement, however many rows it answers', async ({ statements }) => {
283
+ await store(orgId, 'first');
284
+ await store(orgId, 'second');
285
+ const before = statements.count();
286
+ await target.as(member, { limit: 50 });
287
+ // On this driver as on Postgres: a read added per row in the handler is a third statement here.
288
+ expect(statements.count() - before).toBe(1);
289
+ });
252
290
  `;
253
291
 
254
292
  /**
@@ -325,11 +363,30 @@ export interface QueryOptions extends FeatureTarget {
325
363
  readonly sliceRepo?: string;
326
364
  }
327
365
 
366
+ /**
367
+ * Declared beside its one thrower, as `backfill.ts`'s twin is: the read imports the slice's own row
368
+ * type and repo, and a slice declaring another table under its name has neither.
369
+ */
370
+ export class QuerySliceEntityError extends UltimateError {
371
+ constructor(input: { readonly path: string; readonly binding: string; readonly table: string }) {
372
+ super({
373
+ code: 'X_QUERY_SLICE_ENTITY',
374
+ cause: `x g query reads the slice's own scaffolded table, and its entity.ts declares ${input.binding} (${input.table}) instead — the generated read would import a row type and a repo that do not exist`,
375
+ fix: `x queries list --json # then declare query({ sql: … from('${input.table}', …) }) by hand in ${input.path}, over ../entity's ${input.binding}`,
376
+ meta: { binding: input.binding, table: input.table },
377
+ });
378
+ }
379
+ }
380
+
328
381
  export function queryFiles(rawName: string, target: QueryOptions): readonly GeneratedFile[] {
329
382
  const name = names(rawName);
330
383
  const feature = names(target.feature);
331
384
  const live = target.live === true;
332
385
  const dir = `${target.surfaceDir}/${target.feature}/${live ? 'live' : 'queries'}`;
386
+ const foreign = foreignSliceEntity(target.feature, target.sliceEntity);
387
+ if (foreign !== undefined) {
388
+ throw new QuerySliceEntityError({ path: `${dir}/${name.kebab}.ts`, ...foreign });
389
+ }
333
390
  const shape: ReadShape = {
334
391
  live,
335
392
  storesRow: sliceTakesScaffoldRow(target, target.sliceEntity, target.sliceRepo),
@@ -137,7 +137,9 @@ const writer = testActor('writer', {
137
137
  }).actor;
138
138
 
139
139
  contractTest('create${feature.pascal} passes the action contract', async () => {
140
- for (const contract of target.contract()) await contract.run();
140
+ // The fourth assertion runs the handler as the writer on 16 generated legal inputs, each
141
+ // inside a transaction that rolls back, so nothing it creates outlives the test.
142
+ for (const contract of target.contract({ exercise: { actor: writer } })) await contract.run();
141
143
  });
142
144
 
143
145
  contractTest('create${feature.pascal} needs an actor with an org', async () => {
@@ -46,6 +46,7 @@ import {
46
46
  createContext,
47
47
  createEffect,
48
48
  createMemo,
49
+ createRoot,
49
50
  createSignal,
50
51
  onCleanup,
51
52
  useContext,
@@ -146,17 +147,27 @@ function ${feature.pascal}FormBody(props: ${feature.pascal}FormProps): JSX.Eleme
146
147
  * registers. Delete the line and the first \`<UiProvider>\` render throws X_UI_RUNTIME_MISSING —
147
148
  * loud on purpose, because a DOM render that lost its runtime is a theme toggle that does nothing.
148
149
  *
149
- * Six NAMED imports, never \`import * as solidRuntime\`: a namespace object handed to a function
150
+ * Seven NAMED imports, never \`import * as solidRuntime\`: a namespace object handed to a function
150
151
  * keeps every export of solid-js alive, and the bundler cannot shake what it cannot see unused —
151
152
  * measured at 14.8 kB minified per island chunk (5.6 kB gzipped) for the namespace form.
152
153
  *
153
154
  * The shell is cleared first: Solid's \`render\` APPENDS when the container already has children,
154
155
  * so without it the server's markup stays on screen above a second, live copy of the same thing.
155
156
  */
156
- export function mount(el: HTMLElement, props: ${feature.pascal}FormProps): void {
157
- setSolidRuntime({ createContext, useContext, createSignal, createMemo, createEffect, onCleanup });
157
+ export function mount(el: HTMLElement, props: ${feature.pascal}FormProps): () => void {
158
+ setSolidRuntime({
159
+ createContext,
160
+ useContext,
161
+ createSignal,
162
+ createMemo,
163
+ createEffect,
164
+ onCleanup,
165
+ createRoot,
166
+ });
158
167
  el.textContent = '';
159
- render(
168
+ // Solid's disposer, returned: the client router calls it when it navigates away, and a mount
169
+ // that returns nothing is X_ISLAND_DISPOSER_MISSING.
170
+ return render(
160
171
  () => (
161
172
  <UiProvider locale={props.locale}>
162
173
  <${feature.pascal}FormBody {...props} />
@@ -0,0 +1,83 @@
1
+ // `x g resource`'s list page, read by Postgres: the generated test that renders the same sorted,
2
+ // paged list as `page.test.ts` with the app's migrated database installed under the handle.
3
+
4
+ import { sortedImports } from './imports';
5
+ import type { ResourcePageOptions } from './resource-page';
6
+
7
+ /** The org the rows belong to — the same one `page.test.ts` writes under. */
8
+ const ORG = '00000000-0000-4000-8000-000000000002';
9
+
10
+ /**
11
+ * The same sort and the same pages, read by Postgres: the app's migrated database installed under
12
+ * the handle, so the query's SQL — `order by`, the keyset seek — answers instead of the memory
13
+ * thunk. The database is embedded Postgres (PGlite) booted from the migrated template, so the
14
+ * proof needs no server and runs on the `unit` step.
15
+ */
16
+ export const pageDbTest = (options: ResourcePageOptions): string => {
17
+ const { feature } = options;
18
+ const path = feature.pluralKebab;
19
+ return `// /${path} against the app's migrated database: the read's SQL path, where page.test.ts is the
20
+ // in-memory one. The same rows, sorted and paged by link, come back in the same order.
21
+ ${sortedImports([
22
+ "import { migratedDatabase } from '@ultimat3/cli';",
23
+ "import { ctxOf, frozenClock, runWithContext } from '@ultimat3/core';",
24
+ "import { setDbClient } from '@ultimat3/db';",
25
+ "import { testActor } from '@ultimat3/policy';",
26
+ "import {\n afterAll,\n beforeAll,\n expect,\n renderRoute,\n reusableDatabase,\n unitTest,\n} from '@ultimat3/testing';",
27
+ ])}
28
+ // The app's API, as boot loads it: \`defineApi\` names the query the page reads.
29
+ import '../../api';
30
+ import * as repo from '../${feature.kebab}/repo';
31
+ import * as page from './page';
32
+
33
+ // A template restore is ~0.5 s; its first build (initdb plus every migration) is what this bounds.
34
+ const BOOT_MS = 120_000;
35
+
36
+ const workerDatabase = reusableDatabase(() => migratedDatabase({ root: import.meta.dir }));
37
+ beforeAll(async () => {
38
+ setDbClient(await workerDatabase());
39
+ }, BOOT_MS);
40
+ // Back to memory, so this file's database never reaches the next file's unit tests.
41
+ afterAll(() => setDbClient(undefined));
42
+
43
+ const url = 'https://example.test/${path}';
44
+ const orgId = '${ORG}';
45
+ // The read grant, named once so the line below fits the formatter's width at any feature name.
46
+ const read = '${feature.kebab}:read';
47
+ const member = testActor('member', { orgId, permissions: [read] }).actor;
48
+ const clock = frozenClock('2026-01-01T00:00:00.000Z');
49
+
50
+ const store = (title: string) => {
51
+ const draft = { orgId, title, price: { minor: 1200, currency: 'USD' } };
52
+ return runWithContext(ctxOf({ actor: member, clock }), () => repo.insert(draft));
53
+ };
54
+
55
+ /** The anchor a pager side renders, read off the HTML: \`rel\` names the side. */
56
+ const linkTo = (html: string, rel: string): string | undefined => {
57
+ const anchor = new RegExp(\`<a\\\\b[^>]*\\\\brel="\${rel}"[^>]*>\`).exec(html)?.[0];
58
+ return /href="([^"]*)"/.exec(anchor ?? '')?.[1]?.replaceAll('&amp;', '&');
59
+ };
60
+
61
+ unitTest('Postgres sorts and pages the list as the memory read does', async () => {
62
+ for (const title of ['beta', 'gamma', 'alpha']) {
63
+ await store(title);
64
+ clock.advance(1000);
65
+ }
66
+ // Unsorted is the query's own order, newest first.
67
+ const newest = await renderRoute(page, { url, actor: member });
68
+ expect(newest.text.indexOf('alpha')).toBeLessThan(newest.text.indexOf('gamma'));
69
+ expect(newest.text.indexOf('gamma')).toBeLessThan(newest.text.indexOf('beta'));
70
+
71
+ // By title, two to a page: the first two titles, then the third through the cursor.
72
+ const sorted = \`\${url}?sort=title&dir=asc&limit=2\`;
73
+ const first = await renderRoute(page, { url: sorted, actor: member });
74
+ expect(first.text.indexOf('alpha')).toBeLessThan(first.text.indexOf('beta'));
75
+ expect(first.text).not.toContain('gamma');
76
+ const next = linkTo(first.html, 'next');
77
+ const second = await renderRoute(page, { url: new URL(next ?? '', url).href, actor: member });
78
+ expect(second.text).toContain('gamma');
79
+ expect(second.text).not.toContain('alpha');
80
+ expect(linkTo(second.html, 'next')).toBeUndefined();
81
+ });
82
+ `;
83
+ };