@ultimat3/cli 29.4.0 → 30.1.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 +1314 -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
package/wiki/SEO.md ADDED
@@ -0,0 +1,288 @@
1
+ # SEO
2
+
3
+ Metadata, JSON-LD, sitemaps, `robots.txt`, feeds and responsive images, each rule an `assert*` that
4
+ throws a coded `SeoError` naming the route file and the edit. Package `@ultimat3/seo` (tier 1) —
5
+ the full reference is
6
+ [`packages/seo/README.md`](https://github.com/developerz-ai/ultimate/blob/main/packages/seo/README.md).
7
+ Route metadata itself is declared on `defineRoute({ meta })` ([Routes and render
8
+ modes](Routes-And-Render-Modes)).
9
+
10
+ ```ts
11
+ export const config = defineRoute({
12
+ render: 'isr',
13
+ meta: ({ post }) => ({
14
+ title: post.title,
15
+ titleTemplate: '%s — Ultimate',
16
+ description: post.excerpt,
17
+ og: { type: 'article', image: post.cover, publishedTime: post.publishedAt },
18
+ ld: [ld.Article({ headline: post.title, datePublished: post.publishedAt, author: post.author })],
19
+ }),
20
+ });
21
+ ```
22
+
23
+ ## What the rules refuse
24
+
25
+ | Code | Trigger |
26
+ |---|---|
27
+ | `X_SEO_META_MISSING` | a `site/` route with no `meta.title` or `meta.description` |
28
+ | `X_SEO_DUPLICATE_META` | two routes share a title or a description |
29
+ | `X_SEO_META_TOO_LONG` | title over 60 characters, description over 160 |
30
+ | `X_SEO_CANONICAL_MISMATCH` | `meta.canonical` does not resolve to the route's own URL |
31
+ | `X_LD_INVALID` | a JSON-LD node missing a required schema.org field — and a required field is required in the builder's **input type**, so `ld.Article` without `datePublished` does not compile. A CMS `null` in one is the same coded refusal |
32
+ | `X_SITEMAP_TOO_LARGE` | the sitemap index past 50,000 files |
33
+ | `X_SITEMAP_ENTRY_INVALID` | a page's `lastmod` that is not a `Date` or ISO 8601 with a zone, or a `locales` name the app does not route — names the route file |
34
+ | `X_SEO_LINK_INVALID` | a `meta.links` entry whose href is not a path or an `http(s)` URL (`javascript:`, `data:` …), a `preload` with no `as`, or a font preload with no `crossorigin`. Thrown while the page renders (SSR and prerender) and reported at the route file by the `seo` step |
35
+
36
+ `x verify`'s `seo` step runs `validateMeta` over every `site/` route: missing, duplicate and
37
+ over-long meta fail the gate. Canonical checks are skipped there, because an app declares no base
38
+ URL; JSON-LD, sitemaps and robots are enforced where they are built — each builder throws. Performance
39
+ budgets are **not** here: they are the `budgets` step and `X_BUDGET_EXCEEDED`.
40
+
41
+ ## Head links
42
+
43
+ `meta.links` puts `<link>` tags in `<head>` — a font preload, a preconnect, a feed — typed, in
44
+ declaration order, for SSR and prerender alike. `canonical` and hreflang keep their own fields, and
45
+ the surface stylesheet is the renderer's, so neither `rel` is spellable here.
46
+
47
+ ```ts
48
+ export const config = defineRoute({
49
+ render: 'static',
50
+ meta: () => ({
51
+ title: 'Acme',
52
+ description: 'Notificaciones electrónicas con constancia de entrega',
53
+ links: [
54
+ {
55
+ rel: 'preload',
56
+ href: asset('assets/fonts/inter-var.woff2'),
57
+ as: 'font',
58
+ type: 'font/woff2',
59
+ crossorigin: 'anonymous',
60
+ },
61
+ { rel: 'alternate', href: '/feed.xml', type: 'application/rss+xml' },
62
+ ],
63
+ }),
64
+ });
65
+ ```
66
+
67
+ Fields: `rel`, `href`, `as`, `type`, `crossorigin`, `media`. Two links of one `rel` are two tags:
68
+ a link's `href` is part of its identity when the head is deduped.
69
+
70
+ ## The social card
71
+
72
+ A document that may not be indexed carries no card — no `og:*`, no `article:*`, no `twitter:*` tag,
73
+ declared or derived — unless the route declares `socialCard: true`. `meta.robots: { index: false }`
74
+ says so, and so does a 4xx or 5xx, which `@ultimat3/render` marks `noindex` by construction. A
75
+ document that may be indexed carries its card whatever the field says. `As of 2026-10`.
76
+
77
+ `meta.socialCard: true` is the route's own opt-in, and the only one. With it, a noindex document
78
+ carries the card exactly as an indexable one does: declared values win, and the derived ones come
79
+ with it — `og:type`, `og:title` and `og:description` from the page's own title and description,
80
+ `og:url` from the canonical, `og:locale`, and the `twitter:card` default. The robots directives
81
+ never change: `renderMeta()` still answers `<meta name="robots" content="noindex,follow">`, and the
82
+ route stays noindex.
83
+
84
+ ```ts
85
+ import { defineRoute } from '@ultimat3/render';
86
+
87
+ export const config = defineRoute({
88
+ render: 'isr',
89
+ revalidate: { ttl: '5m' },
90
+ offline: 'network-only',
91
+ meta: () => ({
92
+ title: 'Pricing',
93
+ description: 'What the plan costs.',
94
+ robots: { index: false },
95
+ socialCard: true,
96
+ }),
97
+ });
98
+ ```
99
+
100
+ ### A card the build photographs: `og.capture`
101
+
102
+ `og: { capture: '/og/home' }` names a `site/` page — one you write, laid out at exactly 1200 x 630,
103
+ `robots: { index: false }` — and `x build --target static` photographs it into `/_og/og/home.png` and
104
+ stamps `og:image`, `og:image:width` and `og:image:height` on every exported document that declares
105
+ it. `image` and `capture` are one or the other; both together is a type error. `As of 2026-11`.
106
+
107
+ | Fact | Rule |
108
+ |---|---|
109
+ | the path | a concrete page path. A dynamic card (`/og/post/[slug]`) lists its pages in its own `prerender()` |
110
+ | no such page | `X_SEO_CARD_ROUTE_UNKNOWN` — from `x verify`'s `seo` step, and again from the build for a path only a dynamic route resolves |
111
+ | the browser | the CLI's own CDP driver: `CHROME_PATH`, `PUPPETEER_EXECUTABLE_PATH` or `SCRAPE_CDP_URL`. None: `X_SEO_CARD_BROWSER_MISSING` |
112
+ | `x build --no-capture` | stamps the same tags, takes no picture, needs no browser. `x verify`'s own build and the reference-app gate pass it, and only they do |
113
+ | a served document | carries no `og:image` for a capture, on purpose: the PNG exists only in the export, the container holds no browser to take one (axiom 7), and an `og:image` naming a file nothing serves is a broken preview a crawler caches. Deploy `site/` as the static export, or set `og.image` — `twitter:card` falls back to `summary` |
114
+
115
+ ```ts
116
+ import { defineRoute } from '@ultimat3/render';
117
+
118
+ export const config = defineRoute({
119
+ render: 'static',
120
+ offline: 'network-only',
121
+ meta: () => ({
122
+ title: 'Postly',
123
+ description: 'A team blog that behaves itself.',
124
+ og: { capture: '/og/home' },
125
+ }),
126
+ });
127
+ ```
128
+
129
+ A card for a ROW published at runtime is the app's job, not the build's: `scrape()` over the row's
130
+ card page, `page.screenshot()`, `disk.put()`, then `og: { image }` with the stored object's URL —
131
+ [Scraping](Scraping#pages-as-files-pdf-and-cards).
132
+
133
+ ## The pieces
134
+
135
+ | Call | Answers |
136
+ |---|---|
137
+ | `renderMeta()` | the head tags: title (a `$` in a title is kept verbatim), canonical, robots, `og:*`, `twitter:*`, hreflang + `x-default`, `theme-color` per scheme, `links`. A document that may not be indexed carries no `og:*`/`article:*`/`twitter:*` tag unless the route declares `socialCard: true` — [The social card](#the-social-card) |
138
+ | `buildSitemap()` | from the route table and each route's `prerender()`, per-locale alternates; one urlset, an index of named `sections`, or a split past a cap (50,000 URLs, 50 MB) |
139
+ | `buildRobots()` | **fail-closed**: only the literal `production` environment opts into indexing; anything else — staging, a laptop, an unset variable — is `Disallow: /` with no sitemap line. In production `disallow` is added to **every** group, and a `User-agent: *` group is emitted to carry it when none is declared |
140
+ | `buildFeed()` | RSS 2.0, Atom and JSON Feed from one item list. The channel's `author`, `copyright` and `icon` reach Atom as `<author>`, `<rights>` and `<icon>`; an item's `author` is `<author>` in Atom and, in RSS, `<author>` with an email or `<dc:creator>` without one; an item's `image` is an Atom enclosure link and an RSS `<media:content>`. An item date must be ISO-8601 with an offset or `Z`; one that is not is treated as absent (and an offsetless one is logged as `seo.feed.date_offsetless`), never read through the server's zone |
141
+ | `builtinImageDriver({ read })` | resize and blur placeholder over core's pipeline — decodes `png`, `jpeg`, `webp`, `gif`; encodes `png`, `jpeg`, `webp`. `avif` needs a CDN driver (`X_IMAGE_UNSUPPORTED`), so `responsiveImage()` offers `webp` only unless given `formats`, and no width past 8192 (`MAX_IMAGE_WIDTH`). `As of 2026-10` |
142
+ | `parseImageQuery()` | the one reader of the `?w=&f=&q=` a responsive image URL carries (`X_IMAGE_QUERY_INVALID`) |
143
+ | `setStaticExport()` | the static build's seam: while it renders, `responsiveImage()` names `/_img/<source hash>-<width>.<ext>` files and `og.capture` names `/_og/<path>.png`. Unset in a served process — [Static assets](Static-Assets#responsive-images-in-a-static-export) |
144
+
145
+ Every `X_SEO_*` and image code is in [Error codes](Error-Codes).
146
+
147
+ ## `robots.txt` and `sitemap.xml`
148
+
149
+ One answer, served two ways. `siteSeo()` from `@ultimat3/cli` builds both from the route table. It
150
+ takes the public `site/` routes (no `policy`) and expands a dynamic route through its `prerender()`.
151
+
152
+ | Where | How |
153
+ |---|---|
154
+ | static export | the scaffolded `apps/web/prerender.ts` writes `siteSeo()`'s files into `.x/static` (`writeSiteSeo`). A dynamic route lists exactly the pages the build emitted |
155
+ | web role (`x dev`, `runRole`) | `GET /robots.txt`, `GET /sitemap.xml` and its parts `GET /sitemaps/:file`, `application/xml`, `public, max-age=3600`, built once per origin and kept for that same hour. Every file shares one answer: a refresh that fails or hangs keeps the previous index **and** its parts. Absolute against `APP_URL`, else `SITE_ORIGIN`, else the request's own origin |
156
+
157
+ Production `robots.txt` rules come from `app.config.ts`: `seo.robots.disallow` (every crawler, in
158
+ every group) and `seo.robots.groups` (per `User-agent`) —
159
+ [Configuration](Configuration#site-and-seo). A crawler obeys only the group naming it most
160
+ specifically, so a group for `GPTBot` replaces `*`'s rules for it. Any other environment emits
161
+ `Disallow: /` alone, whatever either says.
162
+
163
+ ### What is left out
164
+
165
+ | Left out | Decided by |
166
+ |---|---|
167
+ | a page whose `meta` says `robots: { index: false }` | the route's own `meta`, read **once per routed locale**: `noindex` in English only lists the other locales and drops English from their hreflang cluster. Read for a route with no params and no `load` — the only `meta` that resolves without a request |
168
+ | a page of a dynamic route, or of a route with `load` | its `prerender()` entry: `locales: ['es-co']` lists it there only, `locales: []` nowhere. Its `meta` is **not** read — that would be one `load` per page per refresh |
169
+ | a URL `seo.robots.disallow` matches (a `seo.robots.groups` rule does not — it binds only the crawlers it names) | the rule, read as a crawler reads it (prefix, `*`, trailing `$`). A locale copy it blocks leaves the cluster. `As of 2026-10`: such a URL used to be listed — "submitted URL blocked by robots.txt" |
170
+ | a gated route, an `app/` page not in `extra` | `policy`, `seo.sitemap.extra` |
171
+
172
+ ### A page's own `lastmod` and locales
173
+
174
+ `prerender()` answers a bare string, a params object, or an **entry** — the three mix freely:
175
+
176
+ ```ts
177
+ import { defineRoute } from '@ultimat3/render';
178
+
179
+ declare const posts: {
180
+ published(): Promise<readonly { slug: string; updatedAt: Date; englishReviewed: boolean }[]>;
181
+ };
182
+
183
+ export const config = defineRoute({
184
+ render: 'isr',
185
+ revalidate: { ttl: '10m' },
186
+ offline: 'network-only',
187
+ prerender: async () =>
188
+ (await posts.published()).map((post) => ({
189
+ params: { slug: post.slug },
190
+ lastmod: post.updatedAt, // a Date, or ISO 8601 with a zone
191
+ locales: post.englishReviewed ? ['es-co', 'en'] : ['es-co'],
192
+ })),
193
+ meta: ({ params }) => ({ title: params['slug'] ?? '', description: 'A note from the blog.' }),
194
+ });
195
+ ```
196
+
197
+ | Key | Rule |
198
+ |---|---|
199
+ | `params` | the page. The build renders it in **every** routed locale whatever `locales` says: `locales` is about listing, not about which documents exist |
200
+ | `lastmod` | written as W3C Datetime in UTC (`2026-10-01T15:04:05.000Z`). Wins over `seo.sitemap.lastmod`. A time with no zone (`2026-10-01T10:00:00`) or free text is `X_SITEMAP_ENTRY_INVALID`, never read in the server's zone. A moment in the future is **clamped to now**: refusing it would fail the whole sitemap for one row stamped by a clock a second ahead, and writing it teaches a crawler to distrust every date in the file |
201
+ | `locales` | one `<url>` per named locale, hreflang among those only; `x-default` only when the default locale is one of them; a page left in one locale carries no alternates. Omitted: every routed locale. A name the app does not route is `X_SITEMAP_ENTRY_INVALID` |
202
+
203
+ A route with **no params** may answer `prerender()` too — `() => [{ params: {}, lastmod, locales }]`
204
+ — which is how a page with a `load` (a blog index whose date is its newest post's) says either.
205
+
206
+ A key that is none of the three (`lastMod`) is `X_PRERENDER_FAILED`. The static export still lists
207
+ only the pages it emitted; it asks `prerender()` again for what each says of itself.
208
+
209
+ The `<head>` is separate: a page's hreflang tags come from `meta.alternates`, or from every routed
210
+ locale when it declares none. A page listed in one locale declares its own:
211
+ `alternates: [{ hreflang: 'es-CO', href: url }]`.
212
+
213
+ ### Sections: an index by configuration
214
+
215
+ ```ts
216
+ import { defineConfig } from '@ultimat3/core';
217
+
218
+ export const config = defineConfig({
219
+ name: 'acme',
220
+ seo: {
221
+ sitemap: {
222
+ pageSize: 1000,
223
+ sections: [
224
+ { name: 'blog', match: '/blog/**' },
225
+ { name: 'ciudades', match: ['/juzgados/**', '/ciudades/*'] },
226
+ ],
227
+ },
228
+ },
229
+ });
230
+ ```
231
+
232
+ `/sitemap.xml` becomes a `<sitemapindex>`; every listed page goes to `/sitemaps/<name>-<n>.xml`.
233
+
234
+ | Rule | |
235
+ |---|---|
236
+ | match | against the page's path **without** its locale prefix, so `/en/blog/a` files with `/blog/a` and a cluster stays in one section. `/precios` that page · `/blog/*` one segment below · `/blog/**` `/blog` and everything below. First section that matches wins, in declaration order |
237
+ | unmatched | the default section, `pages` — a reserved name |
238
+ | order | `pages` first, then sections as declared; inside one, pages sorted by path, so the bytes depend on which pages exist and never on the order a query returned them |
239
+ | pagination | `pageSize` URLs per file (default and ceiling 50,000); a file past 50 MB splits further. Numbered from 1 even when there is one: `blog-1.xml` keeps its URL when `blog-2.xml` appears |
240
+ | empty | a section with no page writes no file and is not in the index; a site with no page at all answers one empty urlset |
241
+ | index `<lastmod>` | per child, the newest `<lastmod>` inside it; none when no URL in it has one |
242
+ | `robots.txt` | names `/sitemap.xml` alone |
243
+ | refused at boot | a `name` that is not a lower-case slug (`a-z`, `0-9`, single hyphens), a duplicate, `pages`, a `match` that is not a path pattern, a `pageSize` outside 1–50,000 — `X_CONFIG_INVALID` |
244
+
245
+ With no `sections` the output is byte-for-byte what it was: one urlset at `/sitemap.xml`, and past
246
+ a cap (50,000 URLs, or 50 MB) the index with parts `/sitemaps/1.xml`, `/sitemaps/2.xml`, …
247
+ (`SITEMAP_PARTS_DIR`). A part the index does not name is a 404, `no-store`. `As of 2026-10`.
248
+
249
+ ## `llms.txt`
250
+
251
+ `As of 2026-11-06`. **A third file of the same answer, on by default like the sitemap.**
252
+ `/llms.txt` lists the indexable `site/` pages a sitemap lists, by their `meta` title and
253
+ description, in path order, under the home page's title and description:
254
+
255
+ ```
256
+ # Postly
257
+
258
+ > Posts for teams.
259
+
260
+ - [Pricing](https://postly.example/pricing): Plans, per seat.
261
+ ```
262
+
263
+ | Rule | |
264
+ |---|---|
265
+ | served | `GET /llms.txt` from the web role (`text/markdown`, the sitemap's hour of cache); the static export writes the same bytes (`writeSiteSeo`) |
266
+ | listed | public, indexable `site/` pages, in the default locale, less `seo.robots.disallow`. A dynamic route (or one with a `load`) is expanded through `prerender()` page by page, each line the title and description that page's own `meta` gives it through its `load` — the export lists only the pages it emitted, as the sitemap does; a page whose `meta` says `robots: { index: false }` is left out. `prerender()` runs once per answer for the sitemap and this file together |
267
+ | outside production | an empty document, for `robots.txt`'s reason |
268
+ | opt out | `seo: { llms: false }` in `app.config.ts`: an empty document everywhere |
269
+ | build error | while on, a listed page with no `meta.description` is `X_SEO_LLMS_DESCRIPTION_MISSING` in the `seo` step. The step reads `meta` without a database, so it checks the pages whose `meta` needs no `load`; an expanded page with no description is listed by its title alone |
270
+
271
+ The repository's own `llms.txt` is a separate, hand-kept file and is not this.
272
+
273
+ **Beyond `site/`, and `<lastmod>`** (`As of 22.10`) — `seo.sitemap` in `app.config.ts`:
274
+
275
+ ```ts
276
+ seo: { sitemap: { extra: ['/verificar', '/estado'], lastmod: 'git' } },
277
+ ```
278
+
279
+ | Key | Does |
280
+ |---|---|
281
+ | `extra` | lists public `app/` pages beside the `site/` ones — each path must be answered by an `app/` route declaring no `policy`, and is listed per routed locale with the same hreflang cluster and `x-default`. A path no route answers, an `api/` route, a gated page or a `site/` page (listed already) is `X_SITEMAP_EXTRA_INVALID` — refused, never skipped |
282
+ | `lastmod` | `'none'` (default, no `<lastmod>`), `'git'` (the route file's last commit; its mtime without a work tree), `'mtime'`, `'build'` (one timestamp) |
283
+
284
+ The scaffolded `prerender.ts` passes `sitemap` and `root` from `loadSiteSettings(root)`; an app with
285
+ its own `prerender.ts` adds `sitemap: settings.sitemap, root` to its `siteSeo({ … })` call.
286
+
287
+ The parts of a split were `/sitemap-N.xml` once, which no running process could serve
288
+ ([Upgrading](Upgrading)).
@@ -0,0 +1,175 @@
1
+ # Scheduled tasks
2
+
3
+ A `task` is a cron trigger that enqueues jobs. It never does work itself.
4
+
5
+ `As of 2026-08`. Stable API — semver from here ([Upgrading](Upgrading)).
6
+
7
+ ## The canonical shape
8
+
9
+ ```ts
10
+ // task (cron)
11
+ export const nightlyDigest = task({
12
+ cron: '0 3 * * *',
13
+ tz: 'UTC',
14
+ // The occurrence, never the wall clock: a catch-up tick runs long after the instant it fires
15
+ // for, and a payload read off `Date.now()` there is silently for the wrong day.
16
+ enqueue: (occurrenceMs) => [[sendDigest, { occurrenceMs }]],
17
+ });
18
+ ```
19
+
20
+ ## A task only enqueues
21
+
22
+ If it has a handler body, it is a [job](Jobs-And-Workflows), not a task.
23
+
24
+ | Concern | Where it lives |
25
+ |---|---|
26
+ | When to fire | `task.cron` + `task.tz` |
27
+ | What to enqueue | the `enqueue` field — `(occurrenceMs) => [[jobRef, input], …]` |
28
+ | The work | the `job`'s `run` |
29
+ | Retries, steps, concurrency, rate limits | the `job` |
30
+ | Idempotency | the `job`'s `idempotencyKey` — this is what absorbs a double fire |
31
+ | Observability of the run | the job's spans and `x jobs show <id>` |
32
+
33
+ `enqueue` must be cheap and synchronous-shaped: build inputs, return the list. Fanning out per tenant is a job that enqueues children, not a loop with a DB read inside `enqueue`.
34
+
35
+ ```ts
36
+ export const rotateApiKeys = task({
37
+ cron: '15 2 * * 1',
38
+ tz: 'Europe/Berlin',
39
+ enqueue: () => [[fanOutKeyRotation, {}]],
40
+ });
41
+ ```
42
+
43
+ ## The fluent surface
44
+
45
+ Every projection is a method on the task — `nightlyDigest.enqueue()`, never `runTask(nightlyDigest)` — and every declared field is lifted onto it. A task has no `.def`.
46
+
47
+ | Member | Is | Rule |
48
+ |---|---|---|
49
+ | `nightlyDigest.entries(occurrenceMs?)` | the `[[jobRef, input], …]` pairs this task fires — the handle's read of the declared `enqueue` field | built **for that occurrence**, not for "now" — a catch-up dispatch runs long after the instant it fires for, and a payload derived from the wall clock there is silently for the wrong day. Defaults to now, the honest answer for the two callers that have no occurrence: a manual `enqueue()` and `describe()` |
50
+ | `.enqueue(options?)` | fire the declared entries immediately | the backfill and "run it again" path — no scheduler, no leader, no tick. Goes through the **same facade `<job>.enqueue` uses**, so it joins the caller's transaction on the same terms. Returns one `{ job, result }` per entry |
51
+ | `.describe()` | the manifest row | `kind`, `name`, `cron`, `tz`, `catchUp`, `maxCatchUp`, `jobs` — the job names, in declaration order, because a task's entries are a sequence and not a set |
52
+ | `.kind` `.name` `.cron` `.tz` `.catchUp` `.maxCatchUp` | the declaration, lifted | readable, and already resolved: `kind` is `'task'`, `catchUp` defaults to `'skip'`, `maxCatchUp` to `10` |
53
+
54
+ A task reaches its jobs only through its own entries, and those entries carry job **handles**, never job names. So the scheduler dispatches with `<job>.idempotencyKeyFor(input)` and `<job>.retry.attempts` read off the handle — a task never restates a job's retry policy and never resolves a job by string.
55
+
56
+ Manual `enqueue()` and a scheduled tick differ in exactly one place: the key. A tick's is occurrence-scoped, `<task>:<occurrenceMs>:<the job's own key>`, so two schedulers or a retried tick cannot double-fire that occurrence. `enqueue()` uses the job's plain key, because a manual run has no occurrence to scope to — and scoping it to whichever occurrence it happened to land in is the one thing that would make a backfill dedupe against a real tick.
57
+
58
+ ## Fields
59
+
60
+ | Field | Required | Rule |
61
+ |---|---|---|
62
+ | `cron` | yes | standard 5-field expression. No seconds field, no `@hourly` aliases — one way to write it |
63
+ | `tz` | yes | explicit IANA zone (`'UTC'`, `'Europe/Berlin'`, `'America/New_York'`). Omitting it is a compile error |
64
+ | `enqueue` | yes | `(occurrenceMs) => [[jobRef, input], …]`. Zero or more pairs; an empty list is a valid no-op tick. Read back through the handle as `entries()`, never as `.enqueue` — that name on the handle is the *fire* method |
65
+ | `catchUp` | no — default `'skip'` | what to do when the scheduler was down across one or more occurrences. `'skip'` collapses them into one dispatch for the **latest** missed occurrence and drops the older ones, `'run-once'` fires a single catch-up for the **earliest** missed one, `'run-all'` fires one per missed occurrence |
66
+ | `maxCatchUp` | no — default `10` | how many occurrences one round of `'run-all'` fires; the rest wait for the next round. `'skip'` and `'run-once'` fire ONE occurrence whatever the gap — the latest missed and the earliest — so neither is capped by it |
67
+ | `name` | no | the export name, stamped by `defineApi({ tasks: [scheduledTasks] })`. A module nobody hands over keeps `anonymous-task-<n>`; a definition carrying its own `name:` keeps that |
68
+
69
+ Nothing else. There is no `timeout`, no `retry`, no `concurrency` on a task — those belong to the job it enqueues. No `queue` either: the queue is the job's, and a per-call override rides on the fire, as `<task>.enqueue({ queue })`.
70
+
71
+ ## `tz` is required for a reason
72
+
73
+ | Without explicit tz | Consequence |
74
+ |---|---|
75
+ | Server-local time | the schedule silently moves when the container's `TZ` changes or a region differs |
76
+ | Fixed offset (`+02:00`) | drifts by an hour twice a year against "3am local" |
77
+ | DST spring-forward gap | a `2:30` local tick has no instant — the framework fires it at the zone's next valid instant |
78
+ | DST fall-back overlap | a `1:30` local tick occurs twice — a fixed time (`30 1 * * *`) fires **once**, on the first occurrence; an interval (`*/5 * * * *`, `0 * * * *` — minute or hour field `*` or `*/n`) runs through both passes, because the repeated hour is real elapsed time |
79
+
80
+ **`tz` is checked against the runtime's own IANA database, not merely for non-emptiness** `As of 2026-08`. The declaration asks `Intl.DateTimeFormat` to resolve the zone, and `Intl` carries the runtime's copy of the tz database — the only check that can tell `America/Bogota` from `Bogota`. So a non-empty string is not a timezone:
81
+
82
+ ```
83
+ X_INVARIANT: task "nightlyDigest" has tz "Bogota", which is not a zone in the IANA tz database
84
+ fix: use the full zone id on task("nightlyDigest"), e.g. tz: 'America/Bogota' — list the valid
85
+ ones with: bun -e "console.log(Intl.supportedValuesOf('timeZone').join('\n'))"
86
+ ```
87
+
88
+ It fails at declaration, not at the first tick. The alternative is what an abbreviation used to do: resolve every occurrence in UTC and run the cron five hours off, silently, forever.
89
+
90
+ Store UTC, schedule against an explicit zone, format at the edge. See [Timezones and dates](Timezones-And-Dates).
91
+
92
+ ## The `scheduler` role
93
+
94
+ Fixed **1**. Cron dispatch only.
95
+
96
+ | Property | Behavior |
97
+ |---|---|
98
+ | Leader election | an **expiring lease row** in `x_scheduler_leader` (`postgresLeaseLeader`), never `pg_try_advisory_lock` — that grant belongs to the **session**, not to the process: it outlives every transaction, survives or dies by the pool's reset policy on release, and either way no node can renew it or prove it still holds one. A stranded lock nobody can release and a lock silently dropped mid-round are both wrong for election. Whoever holds an unexpired row dispatches |
99
+ | Renewal | `acquire()` **is** the renewal, asked every round — tick 1s, lease TTL `DEFAULT_LEADER_TTL_MS` = 30s. A node that stops renewing loses the lease by expiry, with nothing to clean up |
100
+ | Second instance | a warm standby, not a duplicate. It calls `acquire()` every round like the leader does; while another node holds an unexpired row it acquires nothing and dispatches nothing, and the round after that row expires it takes the row and becomes the leader |
101
+ | Leadership lost | logged once as `jobs.scheduler.leadership-lost`; the node keeps polling and takes over on the first round after the holder's lease expires. **The process does not exit**, and neither does a `replicator` container that loses its lock: it stands by unready and re-asks. Only `x dev --role replicator` refuses (`X_REPLICATOR_SLOT_HELD`) |
102
+ | `/readyz` on the standby | there is none. `ROLE=scheduler` binds the metrics port only (`/metrics`, `METRICS_PORT`, default 9090) and no HTTP server, so liveness is that endpoint — the probe [`docs/ops/01-kubernetes.md`](https://github.com/developerz-ai/ultimate/blob/main/docs/ops/01-kubernetes.md) declares. Nothing routes request traffic to a scheduler in the first place |
103
+ | Missed tick (leader down, node paused, clock jump) | **fires late rather than being skipped** |
104
+ | Double fire during handover | absorbed by the enqueued job's `idempotencyKey` — the second enqueue returns the existing handle, no new row |
105
+ | Drain on SIGTERM | registered at the `accept` phase: stop dispatching, wait out the dispatch round in flight, then delete the lease row — the standby promotes on its next round rather than waiting out the TTL, and never onto an occurrence this node is still enqueueing for |
106
+ | Two dispatch rounds at once | impossible in one process. The timer re-arms on the round it finished, and any other caller joins that round instead of opening a second one over the same last-tick state |
107
+ | Durable state | none in the process. Last-tick state is a Postgres row |
108
+
109
+ That chain is the whole safety argument: **at-least-once dispatch + required job idempotency = effectively-once work.** A scheduler that guarantees exactly-once dispatch does not exist; one that guarantees never-silently-skipped does.
110
+
111
+ Role table and drain sequence: [Deployment](Deployment).
112
+
113
+ ## Introspection
114
+
115
+ | Command / tool | Output |
116
+ |---|---|
117
+ | `x tasks list --json` | the descriptor — `name`, `cron`, `tz`, `catchUp`, `maxCatchUp`, `jobs` — plus the resolved **`next`** occurrence, rendered in the task's own `tz`, and its `nextMs` epoch instant; and the **`last`** fire (`last`, `lastMs`, `lastFiredAtMs` — `null` before the first), read from the queue, which `x tasks` opens when a task is declared. All derived, never declared fields |
118
+ | `x tasks show <name> --json` | the same, plus the cron in words (`describe`) and the next N occurrences (`--count`, default 5) |
119
+ | `/admin/jobs/tasks` | the jobs dashboard every `defineAdmin()` serves: schedule, zone, last fire, next fire (`nextTaskRun`), paused; pause, resume and run now behind `job:manage` → [Admin dashboard](Admin-Dashboard#the-jobs-dashboard) |
120
+ | MCP `admin.x_job_tasks.list`, `admin.action.job.task.run-now` | the same screen and actions as admin MCP tools, same authz (`job:read`, `job:manage`) |
121
+ | `/_x` dev panel | the jobs tab: the dev process's tasks beside its queues and runs |
122
+ | `x.manifest.json` | generated `tasks` section — the build-time source of truth |
123
+ | `<task>.describe()` | in process: `kind`, `name`, `cron`, `tz`, `catchUp`, `maxCatchUp`, and the job names it enqueues |
124
+ | `<task>.entries(occurrenceMs?)` | in process: the exact pairs that occurrence fires, inputs included — which `describe()` drops |
125
+
126
+ ```
127
+ $ x tasks list --json
128
+ {"ok":true,"command":"tasks","summary":"1 task(s)","findings":[],"data":[
129
+ {"kind":"task","name":"nightlyDigest","cron":"0 3 * * *","tz":"UTC","catchUp":"skip",
130
+ "maxCatchUp":10,"jobs":["sendDigest"],
131
+ "nextMs":1786503600000,"next":"2026-08-12T03:00:00Z"}]}
132
+ ```
133
+
134
+ `next` carries the task's own zone offset, never a machine-local one: the same `0 3 * * *` in `America/New_York` reads `2026-03-06T03:00:00-05:00` before the spring-forward and `2026-03-09T03:00:00-04:00` after it.
135
+
136
+ ## Testing
137
+
138
+ Frozen clock. `clock.advance` drives cron — never `sleep`, never wall-clock.
139
+
140
+ ```ts
141
+ test('nightlyDigest enqueues one digest job per day', async ({ seed, clock }) => {
142
+ await seed('one-org');
143
+ clock.advance('3d');
144
+ const trace = await runJobs.drain();
145
+ expect(trace.enqueued(sendDigest)).toHaveLength(3);
146
+ });
147
+ ```
148
+
149
+ | Asserted | Why |
150
+ |---|---|
151
+ | Tick count over an advanced window | the cron expression means what you think in the declared `tz` |
152
+ | Idempotency absorbs a replayed tick | advance to the same instant twice; the job row count must not change |
153
+ | DST boundary | advance across a spring-forward and a fall-back in the task's zone |
154
+ | `enqueue` returns the right inputs | the pairs, not the work |
155
+
156
+ Runner: `x test job` (tasks are dispatch, jobs are the assertion surface). See [Testing](Testing).
157
+
158
+ ## Errors
159
+
160
+ | Code | Cause | Fix |
161
+ |---|---|---|
162
+ | `X_IDEMPOTENCY_REQUIRED` | a task enqueues a job with no idempotency key — a double fire would duplicate work | add `idempotencyKey` to the job |
163
+ | `X_FORBIDDEN` | the task's system actor lacks a permission the enqueued action requires | grant it explicitly; a task never bypasses [policies](Policies-And-Authz) |
164
+ | `X_DRAINING` | a tick landed while the leader was releasing its lock | none — the standby promotes and the tick fires late |
165
+
166
+ Full index: [Error codes](Error-Codes).
167
+
168
+ ## Rules
169
+
170
+ - A task never contains a handler body. If it does work, it is a `job`.
171
+ - `tz` is always explicit and always IANA.
172
+ - Every job a task enqueues must be idempotent — that is what makes leader handover safe.
173
+ - Never rely on a task firing at an exact instant. Rely on it firing.
174
+ - Never use a task as a poller for something an [action](Actions) could publish. Cron is a fallback, not an event bus.
175
+ - One task per schedule. Two schedules for one job is two tasks, not a conditional inside `enqueue`.