@ultimat3/cli 29.4.0 → 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 +11 -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 +9 -0
  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 +41 -81
  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 +7 -1
  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
@@ -0,0 +1,379 @@
1
+ # Tutorial 3 — auth and admin
2
+
3
+ Authentication produces one thing: an `Actor`. Nothing downstream authorizes on a session row, a user row or an api key — HTTP, the typed client, jobs, live queries and MCP all read the same actor and hand it to the same `policy`. One authz system, never two.
4
+
5
+ `As of 2026-11-03`. Every command and every output on this page was re-run in the app tutorials 1 and 2 leave — the example `post` slice and the `todo` slice — built from this tree's packages (`scripts/scaffold-smoke-overrides.ts`). The session flow ran against a real Postgres; the admin routes were requested from `x dev`. The reference app excerpt under [Detail pages](#detail-pages-actions-batches) is quoted from `examples/dummy`, not run.
6
+
7
+ Series: [1 — first app](Tutorial-01-First-App) · [2 — first feature](Tutorial-02-First-Feature) · **3** · [4 — jobs and realtime](Tutorial-04-Jobs-And-Realtime) · [5 — deploy free](Tutorial-05-Deploy-Free) · [6 — growing up](Tutorial-06-Growing-Up)
8
+
9
+ ## The three pieces
10
+
11
+ | Piece | Package | Owns |
12
+ |---|---|---|
13
+ | identity | `@ultimat3/auth` | passwords, sessions, OAuth, MFA, api keys → an `Actor` |
14
+ | authorization | `@ultimat3/policy` | permissions, roles, `can()` predicates — the only evaluator |
15
+ | operations UI | `@ultimat3/admin` | the admin app's screens, the `/_x` dev panels |
16
+
17
+ `@ultimat3/auth` and `@ultimat3/admin` are scaffold dependencies: `x new` lists both in the root `package.json` at the version it was released with, beside every other `@ultimat3/*` package. Nothing to add. Every `@ultimat3/*` package releases in lockstep, and a mixed-version install is a combination nobody tested.
18
+
19
+ ## Roles first — they cost one file
20
+
21
+ `x new` writes the role map, `apps/web/shared/roles.ts`: `member` and `admin`, with `admin` inheriting `member`. Each `x g resource` adds its grants there — after tutorial 2, beside the example `post` slice's:
22
+
23
+ ```ts
24
+ // apps/web/shared/roles.ts — the roles, after `x g resource todo`
25
+ import { defineRoles } from '@ultimat3/policy';
26
+
27
+ export const roles = defineRoles({
28
+ member: {
29
+ description: 'Signed in. Reads the app surface.',
30
+ grants: ['dashboard:read', 'post:read', 'todo:read'],
31
+ },
32
+ admin: {
33
+ description: 'Runs the app: the /admin surface, plus everything a member may do.',
34
+ grants: ['admin:read', 'admin:write', 'admin:destroy', 'job:read', 'audit:read',
35
+ 'post:write', 'posts:read', 'posts:write', 'posts:delete',
36
+ 'todo:write', 'todos:read', 'todos:write', 'todos:delete'],
37
+ inherits: ['member'],
38
+ },
39
+ });
40
+ ```
41
+
42
+ Roles are sugar. Everything expands to a flat permission set before any policy runs, so a rule never reasons about the hierarchy, and a cycle is caught once at expansion. `defineRoles()` merges into one map and refuses a role two modules define differently (`X_ROLE_REDEFINED`) — keep every role in this one file. `todo:*` is the policy's permission; `todos:*` is the table's, which the [admin](#the-admin-surface) asks beside its own `admin:*`.
43
+
44
+ ```bash
45
+ bunx x policy list
46
+ ```
47
+
48
+ ```text
49
+ permission roles actions queries
50
+ admin:destroy admin - -
51
+ admin:impersonate - - -
52
+ admin:read admin - -
53
+ admin:write admin - -
54
+ audit:read admin - -
55
+ dashboard:read admin,member - -
56
+ job:delete - - -
57
+ job:manage - - -
58
+ job:read admin - -
59
+ job:write - - -
60
+ post:read admin,member - postList
61
+ post:write admin archivePost,createPost -
62
+ posts:delete admin - -
63
+ posts:read admin - -
64
+ posts:write admin - -
65
+ todo:read admin,member - todoList
66
+ todo:write admin archiveTodo,createTodo -
67
+ todos:delete admin - -
68
+ todos:read admin - -
69
+ todos:write admin - -
70
+ 16 permission(s) no action or query enforces:
71
+ admin:destroy
72
+ …
73
+ ✓ 20 permission(s), 2 role(s), 4 enforced by a declaration
74
+ ```
75
+
76
+ The `actions` and `queries` columns are the reason to run this: a permission enforced by **no action or query** is listed under the table. Here those are the admin's and the jobs dashboard's, which their own screens enforce — for a permission of your own, the list is a grant that does nothing.
77
+
78
+ ## The denial matrix, before you ship
79
+
80
+ ```bash
81
+ bunx x policy explain todo:write
82
+ ```
83
+
84
+ ```text
85
+ action archiveTodo — policy todo:write
86
+ actor verdict deciding reason
87
+ anonymous deny todo:write no actor for todo:write
88
+ admin deny todo:write todo:write predicate returned false
89
+ member deny todo:write actor lacks todo:write
90
+ evaluated with no request input and no row — a rule reading either decides again on the real request
91
+ action createTodo — policy todo:write
92
+ …
93
+ ✓ todo:write — allowed for 0 of 6 actor evaluation(s)
94
+ ```
95
+
96
+ Read the `reason` column, not the verdict:
97
+
98
+ | Reason | Means |
99
+ |---|---|
100
+ | `actor lacks todo:write` | the grant is missing — a role problem, fix it in `defineRoles` |
101
+ | `predicate returned false` | the grant is held, the predicate said no. Here that is tenancy, with no `input.orgId` to compare against outside a request |
102
+ | `no actor for …` | anonymous. Every surface agrees on this one |
103
+
104
+ `<subject>` resolves against a permission, an action name, a query name, then an action's HTTP path — so the `fix:` line an `X_FORBIDDEN` prints is runnable whichever of the four the throwing surface had. Verdicts come from `policyMatrix()` over the app's real `Policy` objects, never a second evaluation.
105
+
106
+ ## Sessions
107
+
108
+ `x new` writes the app's one `Auth`, built on first use because `defineAuth` reads the database and the lockout limiter the boot installs:
109
+
110
+ ```ts
111
+ // apps/web/app/auth/auth.ts, as `x new` writes it
112
+ import { type Auth, defineAuth, postgresAuthAdapter } from '@ultimat3/auth';
113
+
114
+ let built: Auth | undefined;
115
+
116
+ export const appAuth = (): Auth => {
117
+ built ??= defineAuth({ adapter: postgresAuthAdapter() });
118
+ return built;
119
+ };
120
+ ```
121
+
122
+ The defaults: a 30-day absolute and a 7-day idle session, cookie `__Host-x_session`, rotated on a privilege change. `authenticator.ts` beside it resolves that cookie on every request. What the scaffold does **not** write is a sign-in page — see [Known gaps](Known-Gaps).
123
+
124
+ Register, log in, resolve — the real flow, against Postgres, with `const auth = appAuth()` and `org` the seeded demo org (`DEMO_ORG_ID` from `apps/web/shared/demo-org.ts`):
125
+
126
+ ```ts
127
+ await register(auth, { email, password, orgId: org, roles: ['member'] });
128
+ const { actor, token, cookie } = await login(auth, { email, password, ip });
129
+ ```
130
+
131
+ ```json
132
+ {"actor":{"kind":"user","id":"01a1a22f-…","orgId":"9c7ca12f-7aa0-57c4-a1cf-79b3afc8d1dd",
133
+ "roles":["member"],"scopes":[],"permissions":[],"facts":{}},
134
+ "cookie":"__Host-x_session=<token>; Max-Age=2592000; Path=/; HttpOnly; Secure; SameSite=Lax"}
135
+ ```
136
+
137
+ `authenticate(auth, token)` answers the same actor. `permissions: []` is correct — direct grants are for service tokens and break-glass accounts. The role is what carries the set, and `actorHas()` expands it against `roles.ts`:
138
+
139
+ ```json
140
+ {"todo:read":true,"todo:write":false,"admin:read":false}
141
+ ```
142
+
143
+ A `member` reads todos; the scaffold grants `todo:write` to `admin` only, so writing is an edit to `roles.ts`. Anonymous (`anonymousActor()`) holds nothing:
144
+
145
+ ```json
146
+ {"anon":"anonymous","hasRead":false}
147
+ ```
148
+
149
+ ### Signing out clears the browser too
150
+
151
+ `logout(auth, token)` ends the session row. The **response** does the rest, through
152
+ `signOutHeaders()`, in 21.0.0:
153
+
154
+ ```ts
155
+ import { logout, signOutHeaders } from '@ultimat3/auth';
156
+ import { appAuth } from './auth';
157
+
158
+ const auth = appAuth();
159
+ declare const token: string;
160
+ declare const headers: Headers; // the action's ctx.headers
161
+
162
+ await logout(auth, token);
163
+ for (const [name, value] of signOutHeaders({ session: auth.sessions.policy })) {
164
+ headers.append(name, value);
165
+ }
166
+ ```
167
+
168
+ What it appends, verbatim:
169
+
170
+ ```json
171
+ [["set-cookie","__Host-x_session=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Lax"],
172
+ ["clear-site-data","\"cache\", \"storage\""]]
173
+ ```
174
+
175
+ After `logout`, `authenticate(auth, token)` refuses the token: `X_UNAUTHENTICATED`.
176
+
177
+ | Header | Effect |
178
+ |---|---|
179
+ | `set-cookie` (with `session`) | the framework session cookie, expired. An app with its own cookie omits `session` and sets its own |
180
+ | `Clear-Site-Data: "cache", "storage"` (`SIGN_OUT_CLEAR_SITE_DATA`) | the browser drops what the previous member left on the origin: the page store's IndexedDB, local storage, the service worker and its cached pages. **Never `"cookies"`**, which would also clear the signed-out cookie this response sets |
181
+
182
+ **Trade-off, deliberate:** a PWA's offline cache does not outlive the person it was cached for.
183
+ The next load reinstalls the worker and re-precaches. Browsers act on `Clear-Site-Data` only in a
184
+ secure context (HTTPS, or `localhost`). A sign-out with no response at all falls back on the page
185
+ boot, which wipes every stored scope but the current principal's
186
+ ([Realtime](Realtime#tier-3-shipped-in-2100)). The reference app's `endSession` action uses it.
187
+
188
+ ### Failures say one thing
189
+
190
+ Wrong password, unknown address and disabled account are indistinguishable in message **and** duration — the first two below were each run, and print the same `code` and `cause`:
191
+
192
+ ```json
193
+ {"code":"X_UNAUTHENTICATED",
194
+ "cause":"the email and password combination did not match an account — re-enter them before issuing the reset below, which mails a single-use token",
195
+ "fix":"issueVerification(runtime, { purpose: 'password-reset', identifier: email, locale })"}
196
+ ```
197
+
198
+ ### The cookie
199
+
200
+ `__Host-x_session`, from `sessionCookie(token, policy)`.
201
+
202
+ | Attribute | Closes |
203
+ |---|---|
204
+ | `HttpOnly` | XSS reading `document.cookie` |
205
+ | `Secure` | a network attacker lifting it off plaintext |
206
+ | `SameSite=Lax` | CSRF — not attached to cross-site POSTs |
207
+ | `__Host-` + `Path=/` + no `Domain` | a sibling subdomain overwriting it (session fixation) |
208
+
209
+ Absolute and idle expiry are evaluated **independently**: activity never moves the ceiling. Session ids are opaque random tokens and only `sha256(secret)` reaches the database.
210
+
211
+ ### The four actor kinds
212
+
213
+ | Kind | From | Carries |
214
+ |---|---|---|
215
+ | `user` | a session | the row's `roles`, expanded to permissions by policy. Scopes empty — a browser session is not scope-limited |
216
+ | `agent` | an api key | **exactly** the key's scopes. Never the owning user's roles |
217
+ | `service` | machine-to-machine inside the deployment | scopes only, no roles |
218
+ | `anonymous` | no credential | nothing |
219
+
220
+ `resolveActor()` is the single funnel. An agent that can do more than its key says is the failure mode that funnel exists to prevent.
221
+
222
+ ### Auth tables
223
+
224
+ `@ultimat3/auth` exports `AUTH_TABLES` — the DDL `PostgresAuthAdapter` expects, as plain strings, so what
225
+ auth stores is verifiable by reading. **Every boot applies them** (`@ultimat3/cli`'s
226
+ `FRAMEWORK_SCHEMA`), the upgrade of an older `x_users` included, so an app writes no migration for
227
+ `x_users`, `x_sessions`, `x_accounts`, `x_verifications` or `x_api_keys`, `As of 2026-09-23`. The
228
+ per-table constants that were once pasted into migrations are gone (22.0.0).
229
+
230
+ ## The admin surface
231
+
232
+ Two different things share the word.
233
+
234
+ | Surface | Is | Gated by | Available |
235
+ |---|---|---|---|
236
+ | `/_x` | the **dev** dashboard from `@ultimat3/admin/dev` — routes, timeline, live, jobs, db, mail, cache, policy, manifest, services, boundaries (`DEV_PANELS`; `x dev` prints the 11) | dev-only, never mounted in production | in `x dev`, immediately |
237
+ | `apps/admin/` | the app's own admin, served by `defineAdmin()` under `ROLE=web` | the app's role map (`roleAuthz()`), permission by permission | in `x dev`, immediately: a list, a detail and a form per entity, the audit log, and the jobs dashboard at `/admin/jobs`. Measured: `/admin`, `/admin/todos` and `/admin/jobs` answer 200 to the development viewer (`admin`) and 403 with the `myapp_dev_role=member` cookie |
238
+
239
+ The scaffold writes a declaration, not a page, `As of 2026-10`:
240
+
241
+ ```ts
242
+ // apps/admin/app/admin/admin.ts
243
+ import { db } from '@myapp/db';
244
+ import { adminEntitiesOf, defineAdmin } from '@ultimat3/admin';
245
+
246
+ export const admin = defineAdmin({
247
+ // Every entity the handle serves: `x g entity` adds one there, and it is an admin screen on
248
+ // the next boot. Name them one by one instead — `entities: [post]` — to leave a table out.
249
+ entities: adminEntitiesOf(db),
250
+ db,
251
+ });
252
+ ```
253
+
254
+ There is no `page.tsx`, no repo adapter and no screen glue: `defineAdmin()` serves
255
+ `/admin/<table>` from the entity and reads rows through the app's handle. A page file on a path the
256
+ admin mounts is `X_ROUTE_DUPLICATE`; a `defineAdmin()` under `apps/*/src/`, which no boot
257
+ imports, is `X_ADMIN_UNSCANNED`. Who may do what is `apps/web/shared/roles.ts`: `admin:read` +
258
+ `<table>:read` to look, `admin:write` + `<table>:write` to create or edit, `admin:destroy` +
259
+ `<table>:delete` to delete; `x g entity` and `x g resource` grant a new table's three to `admin`.
260
+ `job:read` opens the jobs dashboard and `job:manage` runs its controls. A permission a mounted
261
+ admin route asks for and no role grants is `X_PERMISSION_UNGRANTED` in the `policy` step.
262
+
263
+ ### Per-entity screens
264
+
265
+ ```bash
266
+ bunx x g resource note --admin
267
+ ```
268
+
269
+ `--admin` adds two files to the slice — `apps/web/app/note/admin/resource.ts` and its test — and
270
+ lists the override under `resources:` in `apps/admin/app/admin/admin.ts` in the same run
271
+ (`resources: { notes: noteAdminResource }`, with its import). In this app, which already holds the
272
+ `todo` slice and its wiring:
273
+
274
+ ```text
275
+ ✓ wrote 42 file(s) for resource note — next: …
276
+ ``` The
277
+ override is the only hand-written part; fields, operations, filters and forms derive from the entity.
278
+
279
+ ```ts
280
+ export const noteAdminResource: AdminResourceOptions<AdminRow> = {
281
+ // Columns of the entity, in its own order. A name that is not one is X_ADMIN_FIELD_UNSUPPORTED
282
+ // at boot — delete the line to let the admin pick them.
283
+ listFields: ['title', 'price', 'createdAt'],
284
+ pageSize: 25,
285
+ };
286
+ ```
287
+
288
+ ### Detail pages, actions, batches
289
+
290
+ Everything past the list is declared on the same call. The reference app's operator view
291
+ (`examples/dummy/apps/admin/app/admin/admin.ts`), cut to one resource and one action:
292
+
293
+ ```ts
294
+ import { db, LIVE_RUN_STATUSES, runEvents, runs } from '@postly/db';
295
+ import { api } from '@postly/web/api';
296
+ import { defineAdmin } from '@ultimat3/admin';
297
+ import { useContext } from '@ultimat3/core';
298
+
299
+ export const admin = defineAdmin({
300
+ entities: [runs, runEvents],
301
+ db,
302
+ resources: {
303
+ runs: {
304
+ operations: ['list', 'detail', 'search'],
305
+ scopes: {
306
+ running: { where: [{ field: 'status', op: 'eq', value: 'running' }], count: true },
307
+ failed: { where: [{ field: 'status', op: 'eq', value: 'failed' }], count: true },
308
+ },
309
+ // A `hasMany` of the entity, drawn as `run_events`' own list filtered to this run.
310
+ related: ['run_events'],
311
+ },
312
+ },
313
+ actions: [
314
+ {
315
+ name: 'run.cancel',
316
+ permission: 'run:write',
317
+ entity: runs.$name,
318
+ // Decides the button on each row, and is asked again on the server before `handle`.
319
+ when: (row) => LIVE_RUN_STATUSES.some((status) => status === row['status']),
320
+ // A checkbox per row and an "all matching" choice in the list's batch bar.
321
+ batch: true,
322
+ handle: ({ input }) => {
323
+ const actor = useContext().actor;
324
+ return api.actions.cancelRun.as(actor, {
325
+ orgId: actor.orgId ?? '',
326
+ runId: String(input['id']),
327
+ });
328
+ },
329
+ },
330
+ ],
331
+ });
332
+ ```
333
+
334
+ | Declared | Served |
335
+ |---|---|
336
+ | `sections`, `formGroups`, `fields.<f>.hintKey`, `fields.<f>.on` | titled groups on the detail and the forms; a hint under a control; a field on one side of create/update only |
337
+ | `related` | the related resource's own list — its columns, its row scope, its policy — under the row |
338
+ | `input: t.object({ … })` on an action | the action's own form at `/admin/<entity>/<id>?action=<name>`, 422 with each issue on its field |
339
+ | `when` | the button only where it applies; a forged post is `X_ADMIN_ACTION_NOT_APPLICABLE` (409) |
340
+ | `batch` | checked rows or every row the list's URL matches, each through the button's gate and audited; past `batch.threshold` rows, `admin.batch` jobs on the worker |
341
+
342
+ No island ships for any of it: the checkboxes join the batch form by their `form` attribute and a
343
+ confirmation is a server round trip, so the admin stays at zero JavaScript. Every action is also
344
+ an MCP tool, `admin.action.<name>`.
345
+
346
+ ### The rules the admin never breaks
347
+
348
+ | Never | Why |
349
+ |---|---|
350
+ | a second authz system | two authz systems is how every framework of this shape died |
351
+ | an admin-only user or role table | admins are users with permissions; a parallel identity store gets forgotten in offboarding |
352
+ | a superuser bypass | there is no flag that skips policy evaluation |
353
+ | offset pagination | offset re-scans every page and skips rows under concurrent writes. Keyset only |
354
+ | a SQL console that writes | read-only, statement-capped, row-capped; writes and data-modifying CTEs are `X_MCP_QUERY_REJECTED` |
355
+
356
+ Full surface: [Admin dashboard](Admin-Dashboard).
357
+
358
+ ## Agents inherit the human, exactly
359
+
360
+ An action carrying `mcp: { expose: true }` becomes an MCP tool whose authorization **is** the action's `policy` object — `@ultimat3/mcp`'s projection calls the action's own `invoke`, so the policy evaluated is the very object `createTodo.policy` holds. The actor is the signed-in user's session, so an agent can never exceed the human it acts for. No trusted-tool mode, no second permission table, no "API permissions" screen to get wrong.
361
+
362
+ A tool a caller may not see is absent from `tools/list` and answers ToolNotFound, never Forbidden.
363
+
364
+ ## Where the gate catches you
365
+
366
+ | Mistake | Step | Code |
367
+ |---|---|---|
368
+ | an action with no `policy` | `typecheck` | build error — the field is required |
369
+ | a permission string nothing declared | `typecheck` | `PermissionRegistry` augmentation narrows `can()` |
370
+ | a route reading a table with no org predicate | `contract` | `X_TENANCY_UNSCOPED` |
371
+ | a grant nothing enforces | — | not a gate; read the `x policy list` columns |
372
+ | a permission a mounted admin route asks for that no role grants | `policy` | `X_PERMISSION_UNGRANTED` |
373
+ | an app rule's `can()` naming a permission only `defineAdmin()` declared, never the app's own `definePermissions()` | `policy` | `X_PERMISSION_BORROWED` |
374
+
375
+ ## Next
376
+
377
+ [Tutorial 4 — jobs and realtime](Tutorial-04-Jobs-And-Realtime): a durable job with replayed steps, a cron task with a required IANA zone, and a live query that patches per subscriber.
378
+
379
+ Related: [Policies and authz](Policies-And-Authz) · [Admin dashboard](Admin-Dashboard) · [MCP and AI](MCP-And-AI) · [Configuration](Configuration)
@@ -0,0 +1,263 @@
1
+ # Tutorial 4 — jobs, tasks and live queries
2
+
3
+ Three primitives, three rules. A `job` is durable work with memoized steps. A `task` is a cron trigger that only ever enqueues. A `query` with `live: true` is a bounded, totally-ordered read that patches per subscriber.
4
+
5
+ `As of 2026-11-03`. Every command, generated file and output on this page was re-run in the app tutorials 1–3 leave, built from this tree's packages (`scripts/scaffold-smoke-overrides.ts`), carrying the [tutorial 2](Tutorial-02-First-Feature) `todo` slice. The [capacity figures](#capacity-measured) are the committed benchmark results, quoted, not re-run.
6
+
7
+ Series: [1 — first app](Tutorial-01-First-App) · [2 — first feature](Tutorial-02-First-Feature) · [3 — auth and admin](Tutorial-03-Auth-And-Admin) · **4** · [5 — deploy free](Tutorial-05-Deploy-Free) · [6 — growing up](Tutorial-06-Growing-Up)
8
+
9
+ ## A durable job
10
+
11
+ `x g resource` already wrote one. Its shape is the whole contract:
12
+
13
+ ```ts
14
+ export const reindexTodo = job({
15
+ input: t.object({ id: t.uuid, orgId: t.uuid }),
16
+ tenant: (input) => input.orgId, // the org the body reads as, from its own input
17
+ idempotencyKey: ({ id }) => `reindex-todo:${id}`,
18
+ retry: { attempts: 5, backoff: 'exponential' },
19
+ async run({ input, step }) {
20
+ const row = await step.run('load', () => repo.byId(input.id));
21
+ if (row === null) return { skipped: true };
22
+ await step.run('process', async () => {
23
+ await repo.list(1);
24
+ });
25
+ return { skipped: false };
26
+ },
27
+ });
28
+ ```
29
+
30
+ | Field | Rule |
31
+ |---|---|
32
+ | `step.run('<name>', fn)` | each step's result is stored under its **name**. A retry replays completed steps from storage and re-executes only from the failure. Step names are stable identifiers, not labels |
33
+ | `idempotencyKey` | an at-least-once caller may enqueue twice; the work happens once |
34
+ | `retry` | per-attempt backoff, independent per step |
35
+ | `.enqueue(input)` | the **only** queue path. A job is never run inline |
36
+
37
+ The generated `reindex-todo.job.test.ts` pins the distant invariant rather than the happy path:
38
+
39
+ ```ts
40
+ const first = await reindexTodo.enqueue({ id });
41
+ expect(first.deduped).toBe(false);
42
+ const again = await reindexTodo.enqueue({ id });
43
+ expect(again.deduped).toBe(true);
44
+ ```
45
+
46
+ Its `.job.test.ts` suffix is what runs it in the gate's `job` step rather than `unit` — see [tutorial 2](Tutorial-02-First-Feature#name-the-tests-after-their-step).
47
+
48
+ Enqueue from an action and the outbox makes it transactional: the row and the job commit together, or neither does. `ctx.jobs` is the facade `<job>.enqueue()` resolves through. Full model: [Jobs and workflows](Jobs-And-Workflows).
49
+
50
+ ## A scheduled task
51
+
52
+ ```bash
53
+ bunx x g task daily-digest --feature todo
54
+ ```
55
+
56
+ ```text
57
+ + apps/web/app/todo/tasks/daily-digest.ts
58
+ + apps/web/app/todo/tasks/daily-digest.job.test.ts
59
+ + apps/web/app/todo/tasks/daily-digest.test.ts
60
+ + apps/web/app/todo/jobs/daily-digest-job.ts
61
+ + apps/web/app/todo/jobs/daily-digest-job.job.test.ts
62
+ + apps/web/app/todo/jobs/daily-digest-job.test.ts
63
+ + apps/web/api/index.ts
64
+ + x.manifest.json
65
+ + openapi.json
66
+ ✓ wrote 9 file(s) for task daily-digest
67
+ ```
68
+
69
+ `--feature todo` puts the task in the existing slice; without it the generator opens a `daily-digest` slice of its own. Either way the task and its job are added to `apps/web/api/index.ts`, which is what names them.
70
+
71
+ ```ts
72
+ export const dailyDigest = task({
73
+ cron: '0 3 * * *',
74
+ tz: 'UTC',
75
+ // The org rides in the payload: the job declares its tenant from its own input, and a task has
76
+ // no request behind it whose org could be read instead.
77
+ enqueue: () => [[dailyDigestJob, { id: '…', orgId: '…' }]],
78
+ });
79
+ ```
80
+
81
+ | Rule | Why |
82
+ |---|---|
83
+ | a task **only** enqueues | the work is durable and retryable; a cron that did the work itself loses it on a restart |
84
+ | `tz` is required, IANA | `0 3 * * *` in `America/New_York` is a different instant before and after a DST boundary. There is no ambient default anywhere in the framework |
85
+ | one leader | leadership is an expiring lease row in `x_scheduler_leader`, never an advisory lock — that grant belongs to the **session**, not to the process: it outlives every transaction on the connection and is released only by an explicit unlock, the pool's reset on release, or the connection dying — so a node can neither renew it nor prove it still holds one. A second `scheduler` is a warm standby, not a duplicate |
86
+
87
+ ### Registration names it
88
+
89
+ Export names become primitive names, and only `defineApi` (or `registerTasks`) stamps them. A task no `defineApi` lists keeps a positional name (`anonymous-task-1`, enqueuing `anonymous-job-2`); `x verify`'s `manifest` step refuses that job as `X_JOB_UNREGISTERED`. With the generator's edit to `apps/web/api/index.ts` in place:
90
+
91
+ ```bash
92
+ bunx x tasks list
93
+ ```
94
+
95
+ ```text
96
+ name cron tz catchUp jobs last next
97
+ dailyDigest 0 3 * * * UTC skip dailyDigestJob - 2026-11-04T03:00:00Z
98
+ ✓ 1 task(s)
99
+ ```
100
+
101
+ ```bash
102
+ bunx x tasks show dailyDigest --count 3
103
+ ```
104
+
105
+ ```text
106
+ kind: task
107
+ name: dailyDigest
108
+ cron: 0 3 * * *
109
+ tz: UTC
110
+ catchUp: skip
111
+ maxCatchUp: 10
112
+ jobs: ["dailyDigestJob"]
113
+ last: -
114
+ at 03:00 every day
115
+ 2026-11-04T03:00:00Z
116
+ 2026-11-05T03:00:00Z
117
+ 2026-11-06T03:00:00Z
118
+ ✓ dailyDigest — 0 3 * * * (UTC), next …
119
+ ```
120
+
121
+ Every instant renders in the task's **own** zone, never a machine-local default. Full model: [Scheduled tasks](Scheduled-Tasks).
122
+
123
+ ## Inspecting the queue
124
+
125
+ ```bash
126
+ bunx x jobs list
127
+ ```
128
+
129
+ ```text
130
+ 0 job(s)
131
+ ✓ 0 ready · 0 running · 0 delayed · 0 dead across 0 queue(s)
132
+ ```
133
+
134
+ A dead job is never filtered out of view.
135
+
136
+ | Subcommand | Does |
137
+ |---|---|
138
+ | `list` | queue depth, matching rows, and the dead-letter list |
139
+ | `show <id>` | state, attempt, every step's result, remaining retry delays |
140
+ | `retry <id> --from-step <name>` | drops that step so it re-executes; everything before it replays from storage |
141
+ | `drain --to <driver>` | **planned** — no target ships. Would move `ready`/`delayed`/`suspended` jobs to another **durable** driver; enqueues on the target **before** acking the source. `--to memory` is refused by name (`X_CLI_BAD_FLAG`), `As of 2026-09`: it acked durable rows into a `Map` that died with the command |
142
+
143
+ There is no Redis or NATS **job** driver, `As of 2026-10` — 25.0.0 deleted both all-throw stubs. So `drain` has no target: it exits `X_NOT_IMPLEMENTED` before the queue boots, having moved nothing. Postgres is the shipped driver, and it is the one `x dev` boots.
144
+
145
+ ## A live query
146
+
147
+ The columns this read has an order for, then the read:
148
+
149
+ ```ts
150
+ export const todoListSorts = ['createdAt', 'title'] as const;
151
+ ```
152
+
153
+ ```ts
154
+ export const todoList = query({
155
+ input: t.object({
156
+ limit: t.number.default(50),
157
+ // A closed set, so a sort is a column this read has an order for, never a caller's string.
158
+ sort: t.enum(todoListSorts).default('createdAt'),
159
+ dir: t.enum(['asc', 'desc']).default('desc'),
160
+ }),
161
+ policy: canTodoRead,
162
+ live: true,
163
+ // Opt-in, unlike an action's tool: a read hands rows to an agent, so silence exposes nothing.
164
+ mcp: { expose: true, description: 'todoList — edit this description' },
165
+ sql: ({ limit, sort, dir }) =>
166
+ // `feature.table`, not the kebab plural: `from()` quotes the identifier into the SQL text,
167
+ // and the entity created the table as snake_case.
168
+ from<Todo>('todos', () => repo.list(limit, sort, dir))
169
+ // `repo.list`'s own order: the thunk is what the in-memory driver answers and this chain is
170
+ // the SQL Postgres runs, so a sort that differs here is a page holding different rows.
171
+ .orderBy(sort, dir)
172
+ // The primary key last, in the direction the handle adds it, makes the order TOTAL.
173
+ .orderBy('id', dir)
174
+ .limit(limit),
175
+ });
176
+ ```
177
+
178
+ `live: true` is not free. The gate rejects it without all four:
179
+
180
+ | Requirement | Why |
181
+ |---|---|
182
+ | `orderBy` on a **total** order | the matcher decides *enters / leaves / moves within* the result from the changed row alone |
183
+ | `limit` | an unbounded result has no bounded change buffer and no bounded reconnect snapshot |
184
+ | no `now()` / `random()` | the same `(input, row)` must always yield the same membership answer |
185
+ | no cross-tenant predicate | tenancy comes from `ctx`, not from `input` — otherwise `X_FORBIDDEN` at subscribe |
186
+
187
+ The generated `todo-list.live.test.ts` asserts on the **SQL text**, because that is the contract an agent reads to self-correct:
188
+
189
+ ```ts
190
+ const source = await sourceFor(
191
+ target,
192
+ { limit: 50 },
193
+ {
194
+ actor: null,
195
+ unenforced: 'a scaffolded test asserts the SQL text; the policy is asserted separately',
196
+ },
197
+ );
198
+ const text = source.toSQL().sql.toLowerCase();
199
+ expect(text).toContain('order by "createdat" desc');
200
+ expect(text).toContain('limit');
201
+ // "ordered" is not enough — dropping the id tiebreak leaves an ORDER BY while going
202
+ // non-deterministic under ties.
203
+ expect(text.slice(text.lastIndexOf('order by'))).toContain('id');
204
+ ```
205
+
206
+ ### Policy runs per row, per subscriber
207
+
208
+ Not a subscribe-time gate that then trusts the stream. The same `policy` object is re-evaluated for each delivered patch — which is why policy predicates are **synchronous**: an `await` there would be a database round trip per row per connected client.
209
+
210
+ `toolFrom(todoList)` from `@ultimat3/mcp` is a read tool (`annotations.readOnlyHint: true`) that reads through `todoList`'s own policy. A read hands rows to an agent, so MCP exposure on a query is opt-in — silence exposes nothing.
211
+
212
+ Five projections from the one declaration: HTTP `GET /_x/query/todo-list?limit=50` (`x routes` lists it as `query todoList · todo:read`), a typed client hook, the live subscription, a cache entry keyed by tenant and policy scope, and the MCP read tool. Full table: [Queries and live queries](Queries-And-Live-Queries).
213
+
214
+ ## Which role runs what
215
+
216
+ <!-- no-run: boots every role on a fixed port; the run is the reader's -->
217
+ ```bash
218
+ bunx x dev --once --port 3100
219
+ ```
220
+
221
+ ```text
222
+ roles web, sync, worker, scheduler
223
+ ```
224
+
225
+ The same run logs `sync reachable` with the node on `ws://localhost:3101/_x/sync` and the app's own `http://localhost:3100/_x/sync` in front of it.
226
+
227
+ | Role | Runs | Scales on |
228
+ |---|---|---|
229
+ | `web` | HTTP: pages, actions, queries | RPS |
230
+ | `sync` | live-query websockets, on `$PORT + 1` | concurrent connections — no sticky sessions |
231
+ | `worker` | the job queue | queue demand: jobs held plus jobs due |
232
+ | `scheduler` | cron dispatch, enqueue only | fixed 1, lease-row leader (`x_scheduler_leader`) |
233
+ | `replicator` | logical replication → change feed | 1 per database; opt in with `--role`, needs a real Postgres with `wal_level=logical` |
234
+
235
+ `x dev` co-locates the first four in one process. Isolation is simulated, not skipped: separate ALS contexts, a real Postgres queue, a real SIGTERM drain.
236
+
237
+ ## Debugging, without grep
238
+
239
+ | Question | Panel at `/_x` | CLI |
240
+ |---|---|---|
241
+ | which step failed, what is queued | `jobs` | `x jobs show <id>` |
242
+ | why did this subscriber not get the row | `live` | — |
243
+ | which clause decided, for which actor | `policy` | `x policy explain <subject>` |
244
+ | which tags would this write bust | `cache` | — |
245
+ | where did this request spend its milliseconds | `timeline` | — |
246
+
247
+ `/_x/<panel>?json=1` returns exactly what the tab draws. `live` lists the registered live queries and states plainly that no subscriber list is attached — `@ultimat3/realtime` does not retain a subscriber's matcher trace. A panel whose source is unwired says which half is missing rather than rendering an empty tab as an answer.
248
+
249
+ ## Capacity, measured
250
+
251
+ **Reachability.** 50,000 real WebSocket clients against a **single** `sync` node over the in-process transport, `SIGKILL`ed with no drain: all 50,000 reconnected, 49,981 received a channel patch inside the window, first patch on the reconnected socket at p50 **54.0s** / p90 **105.5s** / max **145.7s**, and 156,851 connect attempts shed by the accept budget before any query path. Committed at [`scripts/bench/results/50k-restart.json`](https://github.com/developerz-ai/ultimate/blob/main/scripts/bench/results/50k-restart.json).
252
+
253
+ **Delivery.** A second run, 10,000 clients and a probe every 200ms, counting gaps in each client's received sequence: **1,666,882 channel patches received, 0 observed sequence gaps** ([`10k-restart-seq.json`](https://github.com/developerz-ai/ultimate/blob/main/scripts/bench/results/10k-restart-seq.json)). A first-patch timer cannot see a lost patch, which is why the second run exists — `As of 2026-08` it is the only one with delivery accounting. It says nothing about 50,000, and its zero is a lower bound: a gap needs a received frame on each side of it, so "no client observed a loss" is the claim, not "nothing was lost" ([Realtime](Realtime)).
254
+
255
+ Both are per-node recovery — neither run crossed NATS, so neither is a multi-node result nor a throughput figure. Detail and limits: [Realtime](Realtime).
256
+
257
+ Realtime tier 3 (local-first) shipped in 21.0.0, and it is declared on the **entity**, not the query: `entity(name, { persist: true })`, default `false`, keeps its records in IndexedDB beside the one outbox ([Realtime](Realtime#tier-3-shipped-in-2100)). `persist` is not a `query()` field — writing it there is a `TS2353` excess property.
258
+
259
+ ## Next
260
+
261
+ [Tutorial 5 — deploy on a free tier](Tutorial-05-Deploy-Free): the scaffolded Dockerfile, `$PORT`, `0.0.0.0`, the health probes, and release-phase migrations — plus what a sleeping free instance does to the `worker` and `scheduler` roles.
262
+
263
+ Related: [Jobs and workflows](Jobs-And-Workflows) · [Scheduled tasks](Scheduled-Tasks) · [Queries and live queries](Queries-And-Live-Queries) · [Realtime](Realtime)