@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
@@ -0,0 +1,552 @@
1
+ # Migrations and backfills
2
+
3
+ One engine per concern. A **migration** changes the shape of a table — schema, in one ledger, applied identically from a laptop to a release phase. A **backfill** changes the rows already in it — data, as a `job`, with resume, ledger, throttle and progress built in. Confusing the two is the usual mistake: a schema change that also needs to touch every existing row is a migration *and* a backfill, never one artifact doing both.
4
+
5
+ `As of 2026-08`. Source: [`packages/db/src/migrate.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/db/src/migrate.ts), [`generate.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/db/src/generate.ts), [`destructive.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/db/src/destructive.ts); [`packages/jobs/src/backfill.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/jobs/src/backfill.ts) and its siblings.
6
+
7
+ ## When to write which
8
+
9
+ | Change | Write |
10
+ |---|---|
11
+ | Add, rename, retype, drop a column or table; add an index or constraint | a **migration** — `x db gen "<name>"` |
12
+ | Add an index to a big, hot table, or drop one from it | a **migration** too — `indexes: [{ on, concurrently: true }]` for the build; every drop is concurrent already. [Indexes built concurrently](#indexes-built-concurrently--expand--contract) |
13
+ | Rewrite, normalize, re-derive or backfill values in rows that already exist | a **backfill** — `backfill({ … })` |
14
+ | Add a nullable column, then populate every existing row before making it required | both: the migration adds the column, the backfill sweeps the rows, a second migration adds the `not null` once the sweep is done (expand → migrate → contract) |
15
+
16
+ ## Migrations: one engine, one ledger
17
+
18
+ **This page documents the code at 2.0.0 and two majors have shipped since** — `npm view @ultimat3/core version`, `As of 2026-08-20`. Three things shipped in 3.0.0 that this page does **not** yet describe: `x db seed` (run `x db seed --help`), `rollback({ steps: -1 })` reverting every migration but the last, and a migration deleted from the tree being invisible to the audit. **And four in 4.0.0**, three of which change generated DDL: `on delete` now reaches the emitted SQL — so any app that ever declared `references(…, { onDelete })` generates a **different** migration and should read `x db gen`'s diff before applying it — a `references()` *removed* from a column now emits a `drop constraint`, drift reports `changed-foreign-key` for a key whose rule moved, and the `x_jobs` idempotency index gains `tenant_id`. Read [`CHANGELOG.md`](https://github.com/developerz-ai/ultimate/blob/main/CHANGELOG.md)'s `3.0.0` and `4.0.0` sections beside this page until it is rewritten, and [Upgrading](Upgrading) for the edits. On a pin still at 1.x, take [Known gaps → `x db gen` / `x db migrate`](Known-Gaps) and its 1.1.0/1.2.0 workarounds instead.
19
+
20
+ `x db gen` and the `ROLE=migrate` release-phase container run the **same** engine — `packages/db`'s `migrate()`/`generateMigration()` — not two. **In 1.1.0** they did not: `x db gen`'s subcommands shelled out to `bunx drizzle-kit`, a second schema engine with its own journal, declared in no `package.json` and fetched unpinned at run time, which is why a 1.1.0 scaffold's own `bin/setup` fails. That shelling-out is gone from current source — `cmd-db.ts` calls `generateAppMigration` and `runMigrations` from `@ultimat3/db`/`@ultimat3/cli` directly, and the only remaining mention of `drizzle-kit` anywhere is a file header comment recording the history.
21
+
22
+ ```
23
+ x db gen "initial" # a new app's first database command — x new writes no migration
24
+ x db gen "add publish_at" # diffs entities against migrations, writes a named migration + its down
25
+ x db gen # nothing to generate: rewrites the schema dump and nothing else
26
+ x db migrate # applies pending migrations, dev or prod, through migrate()
27
+ ROLE=migrate # the same migrate() as a release-phase container, one image
28
+ ```
29
+
30
+ `x db gen` is the **only** writer of `packages/db/migrations`, `As of 2026-08`. `x new` scaffolds no
31
+ `0000_initial.sql`, so a scaffold that **declares an entity** — the default `--example` slice does —
32
+ is red on `x verify`'s `drift` step until that one command runs: *packages/db has a schema but no
33
+ migration recorded it*, fix `x db gen "initial"`. Zero entities against zero migrations is
34
+ agreement, not drift, so `x new --no-example` is green until its first `entity()`. A
35
+ scaffold that hand-wrote the first migration was a second writer of the same directory: the file
36
+ carried no `.snapshot.json`, which only the generator can produce, so the app's first `x db gen` and
37
+ its first `x db migrate` both refused (axiom 1).
38
+
39
+ ### The `x_migrations` ledger
40
+
41
+ Every migration applies inside its own transaction, recorded into `x_migrations` (`LEDGER_TABLE`) as one row: id, name, checksum, `applied_at`, `app_version`, `duration_ms`. A few properties are load-bearing:
42
+
43
+ - **Checksum-pinned.** An applied migration's `up` text is hashed once; if the file on disk hashes differently later, `auditLedger` throws `X_MIGRATION_CONFLICT` rather than silently re-running edited SQL — the fix is a new migration, never an edit of one already shipped.
44
+ - **App-version fenced.** A ledger row from a build this process does not ship is also `X_MIGRATION_CONFLICT` — the contract a rolling deploy needs: a pod must refuse to migrate a database another build already owns, or two schemas race during the rollout.
45
+ - **Except a rollback** (`As of 2026-10-05`, `migrate-rollback.ts`). When every unknown row sorts after the newest migration this build ships, and this build has nothing left to apply, an older image is being rolled back onto a newer build's schema: nothing is applied, the rows are logged as `ultimate migrate ledger ahead of build` and returned in the report's `ahead`. An unknown row between shipped ones, or a shipped migration never applied, is still a conflict.
46
+ - **Advisory-locked, on one pinned session.** An advisory lock is scoped to a Postgres *session*, not a statement, so the lock is taken and held on a single reserved connection for the whole run (`withAdvisoryLock`) — see [Resource management](Resource-Management) for the general `using`-pin shape this follows. `lock: false` is declared as an escape hatch on both option types and, `As of 2026-08`, is passed by nothing in the repo — every shipped migration path takes the lock.
47
+ - **Acquired by a bounded poll, never a blocking wait.** `acquireLock` tries `pg_try_advisory_lock` once per 500ms (`MIGRATION_LOCK_POLL_MS`) against a 60s budget (`MIGRATION_LOCK_WAIT_MS`, overridable per call as `lockWaitMs`), then raises `X_MIGRATE_CONCURRENT`. So a genuinely slow migrator ahead of you is waited out and two overlapping deploys still serialise — but a wedged one becomes an exit code inside the deploy window. Blocking `pg_advisory_lock` has no timeout at all: it held `helm upgrade --wait` inside one statement, printing nothing, with the job never failing so `backoffLimit` never fired.
48
+ - **One statement per `up`/`down` send, script-split.** `applyScript` runs `statementsOf(script)` one at a time inside the migration's own transaction, because the two drivers (PGlite, pooled Postgres) disagree about sending a multi-statement script as one call.
49
+ - **Every migration's own transaction.** `migrate()`/`rollback()` apply and reverse one migration at a time, each in its own transaction, on purpose — a failed `up` must leave the ledger describing exactly the migrations that *did* run, not an all-or-nothing batch. This loop is declared with `expectedQueryLoop` at the source, so the N+1 detector never flags it.
50
+ - **Except one marked `-- transaction: false`** (`As of 2026-10-31`, `migration-step.ts`): run one autocommitted statement at a time, with no `lock_timeout`, its ledger row written after the last statement. `x db gen` writes it for concurrent index work; a hand-written migration may carry it too. Its statements must be idempotent — a failure leaves what ran in place and no row, and the next `x db migrate` runs the file again.
51
+
52
+ ### `generateMigration()` — one diff engine
53
+
54
+ `x db gen` diffs the app's entity snapshots against the schema the newest migration's `.snapshot.json` sidecar recorded, and writes the migration text plus that sidecar for the *next* diff. The sidecar is the generator's diff base, in the entity's vocabulary; what the database holds, in SQL, is [the schema dump](#the-schema-dump). A migration missing its sidecar — deleted, or hand-written without one — is `X_MIGRATION_SNAPSHOT_MISSING`: refused rather than defaulted to an empty schema, which would emit `create table` for every table the database already holds. No migration at all is a different answer and not a refusal: zero migrations declare the empty schema, which is what makes `x db gen "initial"` work in an app that has never generated one.
55
+
56
+ Its two remedies, in the order they are safe to run: `git checkout -- packages/db/migrations/<id>.snapshot.json`, or, when the file was never written, `rm packages/db/migrations/<id>.* && x db gen "<name>"`. The delete comes first because `x db gen` against the migration that is still there raises this same code. Until 2026-08 the `fix:` said "restore … from version control" alone while `x db migrate`'s `unknown-schema` difference answered `x db gen "snapshot <name>"` — each naming the command that raises the other, with nothing to restore in an app whose sidecar was never written.
57
+
58
+ ### Foreign keys are their own statement
59
+
60
+ Every foreign key is `alter table … add constraint`, emitted **after** every table statement, and
61
+ never a `references` clause inside `create table` — decided 2026-08. Inline, the constraint is
62
+ created with the table, so the referenced table has to exist already; the order `generateMigration`
63
+ walks is `describeEntities()`, which is the app's *import* order and says nothing about where a
64
+ `references()` points. Measured against PGlite on a scaffolded app: `create table "comments" (…
65
+ references "posts" …)` ahead of `create table "posts"` is `relation "posts" does not exist` on
66
+ statement one, and `down` had the mirror fault — `drop table "posts"` while `comments` still
67
+ references it is `2BP01`. `down` is reversed as a whole, so the constraint drops pushed last come
68
+ out first.
69
+
70
+ **No topological sort and no cycle error**, deliberately: separate constraints need no ordering at
71
+ all, and two tables referencing each other cannot be expressed inline in any order. The same call
72
+ site answers the second half — a `references()` added to a column that **already exists** now emits
73
+ its own `add constraint`, where before the `up` came out empty, `x db gen` wrote no file, and the
74
+ `drift` step stayed red forever behind a fix that did nothing.
75
+
76
+ **Removing a `references()` emits the `drop constraint`**, `As of 2026-08-19` — it used to emit
77
+ nothing, and that is not the harmless omission a removed index is. A removed index leaves the
78
+ snapshot correct by omission; a removed key left the constraint on the database *and* wrote
79
+ `foreignKeys: []` beside it, so the record actively denied a constraint the catalog held, and
80
+ `compareForeignKeys` judges only the declared side. The drop names the constraint the **previous
81
+ snapshot** recorded rather than the name this generator would have chosen, so a hand-written
82
+ `fk_legacy` is dropped by its own name instead of `42704`. A column being dropped in the same
83
+ migration takes its constraint with it, and no second `drop constraint` is emitted for it.
84
+
85
+ **`on delete` reaches the SQL too**, `As of 2026-08-19`. `references(() => orgs.id, { onDelete:
86
+ 'cascade' })` has type-checked since 1.0 and the clause it produced was `references "orgs" ("id");`
87
+ — a declared cascade that the database refuses the delete under instead. `ColumnDescription` and
88
+ `ReferenceDescription` carry `onDelete`, `addForeignKey` writes the clause, and a rule changed on
89
+ either side is `changed-foreign-key` drift whose `fix:` is the drop/add pair as one `psql -c`
90
+ command, because Postgres has no `alter constraint` for it.
91
+
92
+ The sidecar is written through `snapshotJson()`, not `JSON.stringify(value, null, 2)`. Biome
93
+ collapses a short array onto one line and `JSON.stringify` never does, so an app whose `lint` step
94
+ read that directory failed `x verify` on a file no author typed — *Formatter would have printed the
95
+ following content* — as `X_LINT_FAILED`. Two fixes, both in 2.0.0: the serialiser is a
96
+ fixed point of Biome 2.5.5 at `lineWidth: 100`, and `x new`'s `biome.json` excludes
97
+ `**/migrations`, the glob this repo's own config already carried.
98
+
99
+ ## Indexes built concurrently — expand → contract
100
+
101
+ `As of 2026-10-31`. `create index` holds SHARE on the table for the whole build, so every write
102
+ waits; `drop index` takes ACCESS EXCLUSIVE, so every read waits too. On a big, hot table either one
103
+ stalls the app or trips the migration's 3 s `lock_timeout` mid-deploy. The concurrent forms block
104
+ neither, and Postgres refuses them inside a transaction — so they ship in a migration of their own.
105
+
106
+ ```ts
107
+ import { entity, text, uuid } from '@ultimat3/entity';
108
+
109
+ export const runs = entity('runs', {
110
+ columns: { id: uuid().primaryKey(), status: text({ max: 20 }), bank: text({ max: 40 }) },
111
+ indexes: [{ on: ['status'], concurrently: true }],
112
+ });
113
+ ```
114
+
115
+ ```sql
116
+ -- 20261031120000_index_status
117
+ -- GENERATED by `x db gen` from the app's entities — do not edit.
118
+ -- Editing an applied migration changes its checksum: X_MIGRATION_CONFLICT on the next apply.
119
+
120
+ -- transaction: false
121
+ create index concurrently if not exists "runs_status_idx" on "runs" ("status");
122
+
123
+ -- down
124
+ drop index concurrently if exists "runs_status_idx";
125
+ ```
126
+
127
+ | | |
128
+ |---|---|
129
+ | Build | `concurrently: true` on an `indexes:` entry. On a table the same migration creates it changes nothing — the table is empty and invisible until COMMIT, so it is built in that transaction |
130
+ | Drop | **every** removed index, concurrently built or not: delete it from `indexes`, `x db gen`. A total unique btree (a shape a UNIQUE constraint may back) is first unhooked with `alter table … drop constraint if exists` in the same step, right before its concurrent drop — `drop index concurrently` on a constraint's index is `2BP01`. That ALTER takes ACCESS EXCLUSIVE, so the runner gives it a short transaction of its own under the 3 s `lock_timeout` |
131
+ | Files | no concurrent work (no `concurrently: true` build on a live table, no removed index) → ONE transactional migration, exactly as before. Concurrent work and nothing else → ONE `-- transaction: false` migration. Concurrent work beside other changes → TWO: `<id>.sql` (the transaction: new columns, tables, constraints, plain indexes) then `<id>_concurrently.sql` (the concurrent builds, then every drop). Each has its own snapshot and hash, and `--json` names the second as `followUp` |
132
+ | Order | expand, then contract: inside the step every build runs before any drop or constraint unhook, across tables — a renamed key's replacement exists before the old key goes, so for a moment both exist and never neither |
133
+ | Failure | an interrupted build leaves an INVALID index under the name. The next `x db migrate` finds it (`pg_index.indisvalid`), drops it concurrently, and builds it again; no row was written, so nothing is skipped |
134
+ | Never recorded | `concurrently` is how an index is built, not what it is — not in the name, not in the snapshot. Adding or removing it on an existing index generates nothing |
135
+ | Not destructive | an index holds no rows, so the destructive rail leaves a concurrent drop alone — and still requires `-- destructive: true` for a column or table drop beside it |
136
+
137
+ A hand-written migration with `create index concurrently`, `drop index concurrently` or
138
+ `reindex … concurrently` and no `-- transaction: false` line is refused before `BEGIN`:
139
+ `X_MIGRATION_CONCURRENT_IN_TRANSACTION`.
140
+
141
+ ## Partitioned tables
142
+
143
+ Rule: a partitioned table is a table. Declare it as an entity, `x db gen`, then hand-edit the
144
+ generated `create table`'s closing `);` to `) partition by range ("at");`. `As of 2026-11-06`.
145
+ No `entity({ partition })`: attach, default partition and retention are the app's choices.
146
+
147
+ | Question | Answer |
148
+ |---|---|
149
+ | Primary key | must hold the partition key — `primaryKey: ['id', 'at']`. Postgres refuses any unique index that does not (`0A000`) |
150
+ | Drift | the PARENT is judged: columns, key, indexes, checks, triggers. A partition is not app schema (`pg_class.relispartition`), so next month's is no `unexpected-table` and its `fix:` is never `drop table`. A key Postgres clones per partition (`conparentid <> 0`) is not counted. `introspectSchema()` marks the parent `partitioned: true` |
151
+ | A reference INTO one | no `.references()`. A one-column key is `42830` (no unique index on `id` alone can exist) and a same-tenant key's `(org_id, id)` index is `0A000`. Hand-write the composite key into the migration — `alter table "notes" add constraint "notes_event_fkey" foreign key ("event_id", "event_at") references "events" ("id", "at");` — drift judges declared keys only, so it is no finding. The repository's tenant filter still holds on reads; the database does not enforce the same tenant |
152
+ | Retention | `detach`, then `drop`, in the app's own job — never `retain`, never a `delete` |
153
+ | Next month | an app `task` on a monthly cron (`tz: 'UTC'`) enqueuing a job that runs the statement below, `if not exists` so a double fire is a no-op |
154
+ | `create index concurrently` | refused on a parent (`0A000`): `X_MIGRATION_CONCURRENT_PARTITIONED`, whose fix is the sequence — each partition concurrently, the parent `on only`, then `alter index … attach partition`, in a `-- transaction: false` migration |
155
+ | Schema dump | rendered: the parent with `partition by`, each partition `partition of … for values` in the parent's file. A partition's attached indexes, cloned keys and cloned triggers are not written: the parent's recreate them |
156
+
157
+ The statements the job runs (pinned by `packages/db/src/partition-recipe.test.ts`):
158
+
159
+ ```sql
160
+ -- next month: idempotent
161
+ create table if not exists "events_2026_12" partition of "events"
162
+ for values from ('2026-12-01') to ('2027-01-01');
163
+ -- retention: the month leaves the table first, then its rows go with it
164
+ alter table "events" detach partition "events_2026_11";
165
+ drop table "events_2026_11";
166
+ ```
167
+
168
+ ## The schema dump
169
+
170
+ Rule: the current schema is committed SQL under `packages/db/schema/` — written by `x db gen` and `x db migrate`, never by hand, and held equal to the migrations by `x verify`'s `drift` step (`X_SCHEMA_DUMP_DRIFT`). `As of 2026-10`. Source: [`packages/db/src/introspect-catalog.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/db/src/introspect-catalog.ts), [`schema-dump.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/db/src/schema-dump.ts), [`schema-load.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/db/src/schema-load.ts).
171
+
172
+ | Directory | Holds | One file per |
173
+ |---|---|---|
174
+ | `01_extensions/` | `create extension if not exists` | extension |
175
+ | `02_types/` | enums, domains | type |
176
+ | `03_sequences/` | sequences no column owns | sequence |
177
+ | `04_tables/` | columns, defaults, generated and identity columns, primary key, unique, check; a `serial` column's sequence; `replica identity`; `partition by`, and each partition after its parent | table — a partition rides in its root's file |
178
+ | `05_indexes/` | indexes no constraint backs | table |
179
+ | `06_foreign_keys/` | `alter table … add constraint` | table |
180
+ | `07_views/` | views; materialized views with their indexes | view |
181
+ | `08_functions/` | functions and procedures, overloads together | name |
182
+ | `09_triggers/` | triggers, and a non-default enabled state | table |
183
+ | `10_policies/` | `enable` / `force row level security`, then each policy with every clause (`As of 2026-11-06`) | table |
184
+ | `framework/<same ten>/` | the framework's own tables — `x_jobs`, `x_users`, `x_migrations`, … | — |
185
+ | `unrendered.sql` | comment lines naming what the dump cannot spell | — |
186
+
187
+ - **One producer.** A scratch database, framework tables applied, every migration replayed, then read out of `pg_catalog`. Never the dev database: a table made by hand there is not in the dump. No `pg_dump`.
188
+ - **One engine rule: PGlite unless the migrations require otherwise.** The scratch database is the embedded one. It is a real Postgres only when a migration creates an extension the installed PGlite cannot link — decided from the migrations and the installed package, never from which URL is set, so a laptop and CI choose alike.
189
+ - **Deterministic.** Objects sorted by name in code-unit order, columns in ordinal order, Postgres' own `pg_get_*def` spellings, every sequence option written out. No timestamp, version, owner or grant. Same migrations, same bytes.
190
+ - **A one-table change is a one-file diff.** A file whose bytes did not move is not rewritten.
191
+ - **Load equals replay.** The `drift` step empties the scratch schema, loads the dump, and renders it again; the bytes must match. Loading runs in kind order, framework twin first, and retries a file that names something not created yet (a view over a later view, a default calling a function).
192
+ - **500-line ceiling.** A longer file is split `<name>.1.sql`, `<name>.2.sql`, …; the loader joins the parts in numeric order.
193
+ - **`packages/db/.gitattributes`** (scaffolded): `schema/** linguist-generated=true text eol=lf`. The `eol=lf` is load-bearing — the gate compares bytes.
194
+
195
+ | Not the same thing | Answers | Vocabulary |
196
+ |---|---|---|
197
+ | `<id>.snapshot.json` | what `x db gen` diffs the entities against | the entity's — generator spellings |
198
+ | `packages/db/schema/` | what the database holds | SQL — Postgres' spellings |
199
+ | `x_migrations` | what is deployed | ledger rows, checksummed |
200
+
201
+ When it is owed: from the first migration. An app with no migration and no dump owes none — `x new --no-example` stays green.
202
+
203
+ | `drift` finding | Cause | Fix |
204
+ |---|---|---|
205
+ | `…/schema does not exist` | migrations, and the dump was never generated | `x db gen` |
206
+ | `… differs from what the migrations produce` | a hand edit, a migration written or pulled with no regeneration, or a framework upgrade that changed an `x_` table | `x db gen` |
207
+ | `… is what the migrations produce and is not committed` / `describes nothing the migrations produce` | a file missing, or a stray one | `x db gen` |
208
+ | `the migrations do not replay on the scratch database` | a statement the scratch database refuses | `x db migrate --json` names it, then `x db gen` |
209
+ | `the migrations create extension "…", which the embedded database cannot link … neither TEST_DATABASE_URL nor DATABASE_URL names one` | the engine rule chose a server and none is named | `TEST_DATABASE_URL=postgres://<user>:<password>@localhost:5432/postgres x db gen` |
210
+ | `migration <id> WAS written … only the schema dump failed` | `x db gen` exited 1 **after** writing the migration | repair what the rest of the cause names, then a bare `x db gen` — it writes the dump and no second migration |
211
+ | `a database loaded from the schema dump is not the one the migrations build` | the dump renders an object wrong | `x db gen`, then report the file |
212
+
213
+ Limits, stated:
214
+
215
+ - **Rendered:** the ten kinds above — row security and its policies since `As of 2026-11-06`. **Named in `unrendered.sql`, not rendered:** inheritance children, foreign tables, composite and range types, aggregates, a policy on a relation the dump does not create, rules, and — `As of 2026-10-02` — extended statistics (`create statistics`), a column whose `storage` departs from its type's, a materialized view created `with no data`, and a trigger on a relation the dump does not create (an inheritance child: it used to be filed under `09_triggers/` and the load refused the whole dump). Partitioned tables and their partitions are rendered, `As of 2026-11-06` ([Partitioned tables](#partitioned-tables)). A generated column is spelled `stored` or `virtual` as the catalog says. A dump that names one makes no load-equals-replay claim, and the step does not ask.
216
+ - **Neither rendered nor named:** comments, grants, owners, publications, statistics objects, event triggers, collations, operators, casts.
217
+ - **Bytes are promised per engine major.** `pg_get_*def` spellings move between Postgres majors. On the embedded engine the major is the installed PGlite's (0.5.8 is PostgreSQL 18), pinned by the lockfile. An app on the server engine pins its own: the same major in CI as on the laptops that run `x db gen`, or the `drift` step reports the difference.
218
+ - **`x db reset` still replays.** Loading the dump builds the schema faster in isolation (reference app: 159 ms → 74 ms) and is under 3% of a reset, which is one embedded-database boot.
219
+
220
+ ### Extensions and the scratch database
221
+
222
+ | The migrations create | Scratch database | Needs |
223
+ |---|---|---|
224
+ | no extension | embedded | nothing |
225
+ | an extension the installed PGlite ships — `citext`, `pgcrypto`, `pg_trgm`, `uuid-ossp`, `hstore`, `ltree`, `unaccent`, `btree_gin`, 34 in 0.5.8 | embedded, with it linked | nothing |
226
+ | one it does not — `vector`, `postgis`, `pgstattuple` (0.5.8 ships no `vector`) | a real Postgres: a database created, replayed in, and dropped | `TEST_DATABASE_URL`, else `DATABASE_URL` — the admin URL `@ultimat3/testing` clones its template databases from; the role must be able to create a database |
227
+
228
+ `x dev`'s embedded database links the same list from the same reader, so a migration that replays in the gate applies in `x dev`. An app in the third row cannot run on the embedded database at all and develops against `DATABASE_URL`.
229
+
230
+ ### The boot is cached
231
+
232
+ An embedded scratch boot restores a post-`initdb` snapshot instead of running `initdb`: `.x/cache/pglite-<version>-f1.snapshot`, keyed on the PGlite version alone (one snapshot serves every extension set), about 40 MB, gitignored with the rest of `.x/`. Read only through its own checksum; a file that does not verify, or verifies and will not open, is deleted and rebuilt. Written under a temp name and renamed, so two commands racing leave one whole file. A fresh checkout — CI — pays one `initdb` and writes it. Delete it freely: `rm -r .x/cache`.
233
+
234
+ `x db migrate` also reports `unexpected-object` (`X_DB_DRIFT`): a trigger, function, view, type or sequence the dev database holds and replaying the migrations does not create. Compared by identity, never by definition text. The fix is one command that prints its definition (`psql "$DATABASE_URL" -c '…'`); its comment gives the repair in order — copy the definition into a migration, drop the hand-made copy, then `x db migrate`.
235
+
236
+ ## Foreign keys and CHECKs on a live table
237
+
238
+ `As of 2026-11-02`. A key or a CHECK added as one statement scans the whole table holding a lock that stops writes. `x db gen` writes it in two halves instead, so writes keep flowing.
239
+
240
+ | What | Where it lands |
241
+ |---|---|
242
+ | A foreign key on a live table | the `_concurrently` step, each statement in its own short transaction under `lock_timeout`: index builds, `alter table … drop constraint if exists k, add constraint k … not valid`, `validate constraint`, then the drop of any old key. An old key under another name holds until that last statement |
243
+ | A key on a table this migration creates | the plain `add constraint` in the transaction, byte-identical to before — unless the unique index it needs is built by the deferred step, which would otherwise be `42830`. Then it defers too |
244
+ | A key re-added after a retype | the transaction: the rewrite holds ACCESS EXCLUSIVE already |
245
+ | A CHECK on a live table | `add constraint … not valid` in the transaction (it needs no index), `validate constraint` in the deferred step. New writes are held to it at once. A new table's stays in `create table` |
246
+ | `down` | restore the old key, drop the new key, drop the index |
247
+
248
+ `x db migrate` re-runs a failed step from the top. `validate` finding a bad row is `X_DB_FOREIGN_KEY_VIOLATION` (or the CHECK's own failure) with the constraint in place holding new writes and the old release still serving: repair the row, re-run. A declared constraint left `NOT VALID` in a database whose ledger says the step ran is `X_DB_DRIFT`, kind `not-valid-constraint`, and its fix is the validate statement.
249
+
250
+ ## A column drop takes two releases
251
+
252
+ `As of 2026-11-02`. Old and new pods serve one schema during a roll, and the old release names every column it declares in every read and insert, so a drop in the release that stops declaring it answers `X_DB_SCHEMA_STALE` from every old pod.
253
+
254
+ | Release | The entity | `x db gen` | Writes |
255
+ |---|---|---|---|
256
+ | N | remove it from `columns`, add `retired: ['legacy']` (physical name) | `x db gen "retire legacy"` | `-- retires "t"."legacy"` and no statement. The column stays in the table and the snapshot, marked `retired`, with its indexes, keys and own CHECK |
257
+ | N+1 | remove it from `retired` | `x db gen "drop legacy" --allow-destructive` | `-- contracts "t"."legacy"`, the `drop column`, `-- destructive: true` |
258
+
259
+ | Refusal | When |
260
+ |---|---|
261
+ | `X_MIGRATION_ROLLING_UNSAFE` | a column leaves the entity without having been retired, with or without `--allow-destructive`. Also a retired name no migration records or `columns` still declares, a NOT NULL column with no default or a key column (the new release's inserts would fail, so relax it first), a committed `drop`/`rename` when the `drift` rail is on, and a retire and its drop both pending on a live production database |
262
+ | `X_MIGRATION_IRREVERSIBLE` | the retired column is removed without `--allow-destructive`. Retiring is not consent to losing the rows |
263
+
264
+ An app that stops before it starts (one instance, `strategy: Recreate`) says so: `x db gen "drop legacy" --allow-destructive --rolling-unsafe` writes `-- rolling-unsafe: true` and needs no retire.
265
+
266
+ **The rail.** `rollingSince: "<migration id>"` in `x.verify.json` turns on the `drift` step's fifth rail for every migration after that id: a `drop column` with no `-- retires` in an earlier file, a `rename column`, a `rename table`. A closed list, on statements and not on prose. `x new` writes `"0"`. Unset, the rail does not run, because an applied migration cannot gain a marker (checksum); `x doctor` prints one line naming the id to set. A retire before the bound still earns the drop after it.
267
+
268
+ **A new enum member is not railed.** N-1 pods refuse a value they do not know (`X_INVARIANT_VIOLATED`), but adding a member nobody writes yet is safe, and no file says when the first write ships. Add the member in N, write it in N+1.
269
+
270
+ ## A lock wait is a turn lost, not a failed deploy
271
+
272
+ `As of 2026-11-02`. `lock_timeout` firing during `migrate()` or `rollback()` sends it again, up to `lockRetries` more times (default 5), waiting at least one `lockTimeoutMs` plus jitter so the queue behind the ALTER drains, with a warn line per attempt. A transactional step retries whole, since it rolled back whole; a `-- transaction: false` step retries the one statement, since what ran before it stays run. The ledger row is written only by a success. Exhausted is `X_DB_LOCK_TIMEOUT`, its cause stating the attempts.
273
+
274
+ ## The destructive-migration rail
275
+
276
+ A generated migration that drops a column or table, retypes a column, or truncates a table needs an explicit marker in the file, or `x verify`'s `drift` step refuses to ship it:
277
+
278
+ ```sql
279
+ -- 20260814120000_drop_legacy
280
+ -- destructive: true
281
+
282
+ alter table "posts" drop column "legacy";
283
+
284
+ -- down
285
+ alter table "posts" add column "legacy" text; -- data is not restored
286
+ ```
287
+
288
+ `destructive.ts` owns the classification — a closed list of four kinds (`drop-table`, `drop-column`, `retype-column`, `truncate`), decided against noise-stripped SQL so a comment or a string literal mentioning "drop table" is never mistaken for the operation. `truncate` counts only as the statement's first keyword, so an insert-only hardening migration (`create trigger … before truncate …`, `revoke truncate …`) needs no marker.
289
+
290
+ The criterion is **persisted row data**, not table rewrites: an operation is destructive when applying it removes rows or the values in them. That is why `alter table … drop` names a column and is destructive, while the sub-clauses that name what they drop are excluded — `drop constraint` removes constraint metadata, `drop default`, `drop not null`, `drop identity`, `drop expression` and `drop generated` change column metadata, and a standalone `drop index` removes an auxiliary structure. Every row survives all of them. `retype-column` is on the list for the opposite reason: rewriting a column's type can lose the values it held, whether or not the type is narrower.
291
+
292
+ Only `up` is ever judged — reversing a `create table` is a `drop table`, so a rail reading `down` would mark every migration ever generated, and a marker on all of them marks none.
293
+
294
+ Two separate questions, two codes, asked at two different times:
295
+
296
+ | Code | Asked | Means |
297
+ |---|---|---|
298
+ | `X_MIGRATION_IRREVERSIBLE` | at **generation** | can this migration's `down` restore the rows? A generated drop refuses here first — `x db gen "<name>" --allow-destructive` to proceed anyway |
299
+ | `X_MIGRATION_DESTRUCTIVE` | at the **gate** (`x verify`'s `drift` step) | does the committed `up` destroy data with no `-- destructive: true` line saying so? `x db gen` writes the marker itself when asked to generate a destructive migration |
300
+
301
+ `X_MIGRATION_CONFLICT` is the ledger's own refusal (checksum or app-version mismatch, above) — a third, unrelated code, not a spelling of either of these two.
302
+
303
+ The marker is SQL the migration's checksum covers, so it must be added **before** the migration is applied: marking an already-applied file is an edit, and reaches `X_MIGRATION_CONFLICT` on the next apply, correctly. Full wording for all four codes: [Error codes](Error-Codes).
304
+
305
+ ## Backfills: `backfill()` is a factory over `job()`, not a ninth primitive
306
+
307
+ Same rule `llm()` follows for a model call ([`docs/idea/02-primitives.md`](https://github.com/developerz-ai/ultimate/blob/main/docs/idea/02-primitives.md)): a one-pass sweep over a table is durable background work with an input schema, a retry policy, an idempotency key and a queue — the definition of a `job` — so `backfill()` *returns* one instead of the framework growing a ninth kind of thing. That single line is what gives a backfill `.enqueue()`, the worker's cancellation, the dead-letter path, `x jobs show` and a manifest row for free, with nothing declared twice. See [Jobs and workflows](Jobs-And-Workflows) for the `job` primitive itself.
308
+
309
+ ```ts
310
+ export const normalizePostTitles = backfill({
311
+ name: 'normalize-post-titles',
312
+ tenant: 'none', // a sweep spans every tenant; the PASS opens the cross-tenant scope, not you
313
+ source: ({ ctx }) => db.posts.where({ orgId: ctx.actor.orgId }),
314
+ handle: async ({ rows, signal }) => {
315
+ signal.throwIfAborted();
316
+ await db.posts.upsertAll(
317
+ rows.map((row) => ({ ...row, title: row.title.trim() })),
318
+ { onConflict: ['id'] },
319
+ );
320
+ },
321
+ batch: 1_000, // rows per statement and per durable step — default DEFAULT_BACKFILL_BATCH
322
+ rate: 5, // batches per second — default DEFAULT_BACKFILL_RATE, there is no unthrottled mode
323
+ });
324
+ ```
325
+
326
+ ### Signature
327
+
328
+ ```ts
329
+ backfill<Row>({
330
+ name: string, // REQUIRED — a durable key, unlike a job's
331
+ source(args: { ctx: Ctx }): ReadBuilder<Row>, // the chain to sweep; read once per attempt
332
+ handle(batch: BackfillBatch<Row>): Promise<void> | void,
333
+ batch?: number, // default DEFAULT_BACKFILL_BATCH (1000)
334
+ rate?: number, // default DEFAULT_BACKFILL_RATE, batches/sec
335
+ queue?: string,
336
+ retry?: RetryPolicy, // default DEFAULT_RETRY
337
+ timeout?: DurationInput, // per attempt, not per pass
338
+ requires?: string, // a migration id, checked before a run
339
+ environments?: readonly Environment[], // omitted = every environment
340
+ count?(args: { ctx: Ctx }): Promise<number> | number, // the same predicate, counted
341
+ }): JobHandle<{ force?: boolean }>
342
+ ```
343
+
344
+ `source` returns a `ReadBuilder` — the same chain type [Batching and preloading](Batching-And-Preloading) documents `preload()`/`inBatches()` on — so tenancy, soft delete, the projection and every `.preload()` on it mean exactly what they mean anywhere else. It is read fresh each attempt and never enqueued, so what a run visits cannot drift from what was declared. `handle` is deliberately handed no `step`: a step name minted inside a per-batch body would collide with itself on the second batch. `BackfillBatch<Row>` carries `rows`, `ctx`, `signal` (the run's cancellation composed with this batch's own ceiling) and `index` (0-based position, also the step name — `batch:<index>`).
345
+
346
+ ### A remote source — `pages`, and a pass per scope
347
+
348
+ `pages` beside `source`, exactly one of the two. One page is one durable step that checkpoints `{ cursor, rows: n }` and never the rows, so a pass killed mid-page asks for **that** page again and no earlier one. `next: null` ends the pass; an empty page that does not move the cursor ends it too. `rate` paces the vendor. `count` is optional — a remote may not know.
349
+
350
+ ```ts
351
+ import { backfill, t } from '@ultimat3/jobs';
352
+
353
+ interface Invoice { readonly vendorId: string }
354
+ declare const vendor: {
355
+ invoices(
356
+ account: string,
357
+ cursor: string | null,
358
+ signal: AbortSignal,
359
+ ): Promise<{ readonly items: readonly Invoice[]; readonly nextCursor?: string }>;
360
+ };
361
+ declare const invoices: { upsertAll(rows: readonly Invoice[]): Promise<void> };
362
+
363
+ export const syncInvoices = backfill<Invoice, { readonly accountId: string }>({
364
+ name: 'sync-invoices',
365
+ tenant: ({ input }) => input.accountId,
366
+ input: t.object({ accountId: t.string }),
367
+ scope: ({ accountId }) => accountId, // one pass and one ledger per account
368
+ pages: async ({ cursor, signal, input }) => {
369
+ const page = await vendor.invoices(input.accountId, cursor, signal);
370
+ return { rows: page.items, next: page.nextCursor ?? null };
371
+ },
372
+ handle: ({ rows }) => invoices.upsertAll(rows),
373
+ });
374
+ await syncInvoices.enqueue({ input: { accountId: 'acct-1' } });
375
+ ```
376
+
377
+ | With `scope` | |
378
+ |---|---|
379
+ | ledger row and live idempotency key | `<name>:<scope(input)>` — two scopes, two ledgers, never deduped onto each other |
380
+ | `x db backfill --pending` | one row per scope the ledger holds; a scope never run is not listed — which accounts should sync is the app's |
381
+ | an unscoped declaration | byte-identical to before: key and ledger name are `<name>` |
382
+
383
+ Refused at `backfill()`, before the name is taken: both or neither of `source` and `pages`, or `input` with no `scope` — `X_BACKFILL_SOURCE_INVALID`. The checksum covers `pages` exactly as it covers `source`.
384
+
385
+ ### Idempotency — `handle` is at least once
386
+
387
+ The batch runs inside its own durable step, and the checkpoint is written **after** it returns — never the other way round. An attempt cancelled between the last row and the checkpoint replays that whole page on the next attempt. `handle` must therefore be idempotent: `upsertAll`, `updateWhere`, or any statement whose second run changes nothing — never `count + 1`. What a step persists across a replay is the **cursor and a row count**, never the page itself; `steps.ts` retains a completed step's output for the whole run, and a page held there for every batch of a million-row sweep is every processed row kept in memory until the job ends.
388
+
389
+ Re-enqueueing a backfill that has already run to completion runs a **real job** whose pass is the no-op. The two are different facts and only the second one is dedupe:
390
+
391
+ ```ts
392
+ const first = await normalizePostTitles.enqueue({});
393
+ first.deduped; // false — this is the run
394
+
395
+ // While the first is still LIVE: one live run per name, forced or not.
396
+ const live = await normalizePostTitles.enqueue({});
397
+ live.deduped; // true — the partial index covers ready | delayed | running | suspended
398
+
399
+ // Once the first has COMPLETED: a completed job is in none of those states, so this is not a
400
+ // dedupe. A real job row is created and a worker runs it — the PASS then reads x_backfills and
401
+ // returns { skipped: true, previousRunId }, having touched no row.
402
+ const after = await normalizePostTitles.enqueue({});
403
+ after.deduped; // false
404
+ ```
405
+
406
+ The dedupe is the live-run index; the ledger is what makes the second pass do nothing.
407
+
408
+ `--force` (or `{ force: true }`) is the only override, and it rides the **input**, not the idempotency key — `idempotencyKey` is always `() => definition.name`, so "kick it again" stays one live pass rather than becoming a second writer racing the first. A forced rerun writes a **new** ledger row: history is never overwritten, so what each pass swept stays readable. A completed row is the only one that blocks a rerun — a `running` row is this pass resuming (or the one live idempotency key already holding it), and a `failed` row is an attempt the queue is about to retry anyway.
409
+
410
+ ### The `x_backfills` ledger — `x_migrations`' twin, one level up
411
+
412
+ Keyed by **run**, not by name, because a backfill may legitimately run again where a migration never does. Mirrors the shape of the migration ledger:
413
+
414
+ | Column | Meaning |
415
+ |---|---|
416
+ | `runId` | the job run this pass belongs to — also the ledger's primary key |
417
+ | `name`, `checksum` | the declared name, and a hash of `source`'s and `handle`'s source text (`NUL`-separated, never `batch`/`rate` — pacing is a tuning change, not a different sweep) |
418
+ | `status` | `running` \| `completed` \| `failed` — only `completed` blocks a rerun |
419
+ | `appVersion` | the build that **started** the pass; a redeploy mid-pass does not rewrite it |
420
+ | `rows`, `cursor` | absolute progress — a replayed batch reports the same number, never a delta; `cursor` is `null` before the first batch and once the pass is over |
421
+ | `startedAt`, `completedAt` | epochs; `completedAt` is stamped for `failed` too, and cleared when a retried attempt puts the row back to `running` |
422
+
423
+ A moved checksum **warns** and does not block the run — a bundler that reformats a function body moves its source text with no line of behaviour changed, so this hash is fuzzier than a migration's SQL checksum on purpose, where `@ultimat3/db`'s `auditLedger` throws on the same kind of drift because SQL text is what was actually applied.
424
+
425
+ The ledger is a **report an operator reads**, never a resume source — the step checkpoints inside `backfill-pass.ts` are transactional with the work and decide where a resumed pass restarts; a resume driven off the ledger's own `cursor` would be a second, non-transactional answer to "where were we."
426
+
427
+ ### Throttling
428
+
429
+ `rate` (batches per second, default `DEFAULT_BACKFILL_RATE`) is spent as the **first statement inside the batch's own step**, never wrapped around it — an attempt resuming at batch 500 replays 500 already-completed checkpoints that run no body and touch no database, and paying the full throttle for all of them again before reading a new row would make every resume slower than the original pass. There is no way to turn the throttle off: a backfill that saturates the pool has no correct value here, since it shares that pool with the requests the app is still serving live.
430
+
431
+ ### Progress observability
432
+
433
+ One projection, four surfaces, so none of them can disagree about how far a pass has got:
434
+
435
+ | Surface | Shows |
436
+ |---|---|
437
+ | `x db backfill --list` | the whole ledger, newest first, filterable by name/status |
438
+ | `x jobs list` | ordinary job rows, plus the `backfill()` passes whose ledger row is **`running`** — `jobs-report.ts` filters `{ status: 'running' }`, so a completed or failed pass is not on this surface |
439
+ | `x jobs show <id>` | one job's full state — step trace, next retry — plus this run's ledger row under `backfill` when the job is one |
440
+ | `/_x` jobs panel | the same ledger, live |
441
+
442
+ All four read `inspectBackfills(driver, filter?)` from `backfill-inspect.ts` — there is no second reader. It answers `[]`, never a throw, for a driver whose `JobDriver.backfills` is absent (a driver that ships no ledger, e.g. a hand-rolled one): the surfaces asking about the *queue* must not fail over a fact nobody asked for, though `x db backfill --list` — the surface that **is** the question — says so in its own summary.
443
+
444
+ ### Declared, and never run — the other half of the ledger
445
+
446
+ `x_backfills` answers "which passes have run". Nothing answered "which passes exist", so a cleanup could be authored with `x g backfill <name> --feature <slice>`, merged and deployed, and **never run**, with no surface anywhere saying so. `backfill()` now stamps its own handle — through the same `origin` `WeakMap` `task()` uses, with no `registerBackfill()` call for an app to forget — and the diff between the two halves is a command:
447
+
448
+ ```
449
+ x db backfill --pending --json
450
+ ```
451
+
452
+ Non-zero exit when anything is unswept, so a cron or a deploy check reads the exit code and never the table.
453
+
454
+ | State | Means | Counts as pending |
455
+ |---|---|---|
456
+ | `pending` | declared, and `x_backfills` holds no row under this name | yes |
457
+ | `failed` | the newest pass failed — the queue may already have dead-lettered it | yes |
458
+ | `running` | a pass is in flight | no — an alarm red for the whole of every sweep gets muted within a week |
459
+ | `completed` | a completed row exists, so nothing re-runs without `--force` | no |
460
+ | `excluded` | `environments` does not include this one | no |
461
+
462
+ `orphaned` is the mirror image: a ledger name no declaration carries, i.e. a sweep whose module was deleted after it ran.
463
+
464
+ ### Three optional declarations, all of them data
465
+
466
+ | Field | Rail | Enforced where |
467
+ |---|---|---|
468
+ | `requires: '<migration id>'` | `X_BACKFILL_MIGRATION_PENDING` | `x db backfill`, which is where `x_migrations` is readable — `@ultimat3/jobs` holds no `@ultimat3/db` dependency and does not grow one to read a ledger |
469
+ | `environments: [...]` | `X_BACKFILL_ENVIRONMENT` | **the pass itself**, ahead of the ledger open, so a `.enqueue()` from app code is covered too — and again in `x db backfill` as a pre-check, which refuses before queueing work that would only dead-letter |
470
+ | `count()` | `X_BACKFILL_STALLED` | the pass, once, after the last batch |
471
+
472
+ `environments` ships as declared **data** and never as a hardcoded "cleanups are production": a staging rehearsal is correct practice, so which deploys a sweep belongs to is the app's own convention and this field is only the mechanism carrying it. There is deliberately **no** `dependsOn` graph over other backfills — the real dependency is almost always "after code that tolerates both shapes is serving", which the framework cannot observe and would therefore encode wrongly.
473
+
474
+ `count()` is the same predicate `source` selects on, counted rather than read. It is what makes a dry run honest and turns "did it converge" into arithmetic: a pass that exhausts its source while `count()` still answers above zero has two predicates that disagree, so it **fails** rather than writing a completed row that stops the next deploy re-running it. Left out, the pass converges by definition — the framework will not guess a number on the author's behalf.
475
+
476
+ The **result** is parsed, not trusted: a non-negative safe integer, or `X_INVARIANT` where the count was returned. `NaN > 0` and `-1 > 0` are both false, so an unchecked bad number reads as "nothing left" and writes exactly the completed ledger row this detector exists to prevent.
477
+
478
+ ### Running one — `x db backfill`
479
+
480
+ ```
481
+ x db backfill --list # the x_backfills ledger, newest first
482
+ x db backfill --pending # declared minus completed; non-zero when unswept
483
+ x db backfill normalize-post-titles # DRY RUN — --write is never implied
484
+ x db backfill normalize-post-titles --write # gate, then enqueue; the workers sweep
485
+ x db backfill --all --write # every pending one, isolated per name
486
+ x db backfill normalize-post-titles --write --force # a completed name, into a NEW ledger row
487
+ ```
488
+
489
+ A bare `x db backfill` is refused rather than defaulted: the four shapes answer four different questions.
490
+
491
+ `--write` **enqueues** — it never runs the pass inside the CLI process, because the queue is a job's execution surface. `--all` isolates per name and continues past a failure, exiting non-zero and naming each, so one wedged cleanup cannot block every later one forever.
492
+
493
+ `requires` is checked against `x_migrations` here, and the three answers are distinct on purpose: the ids that are applied, `[]` when the table does not exist (nothing has ever been applied, so every `requires` is unsatisfied and the sweep is refused), and a **propagated error** when the read itself failed. A permission error or a timeout read as "nothing to check" would let a sweep run against exactly the shape it was declared to wait for.
494
+
495
+ ### The `backfill` deploy step triggers; it never gates
496
+
497
+ `backfill` is a **deploy step**, not a `ROLE` — there is no `ROLE=backfill` (`packages/core/src/roles.ts` names six roles). `DEPLOY_ROLES` is `migrate → web → sync → worker → scheduler → backfill`, and the position is the design:
498
+
499
+ | Role | Shape | Why there |
500
+ |---|---|---|
501
+ | `migrate` | `run --rm`, **first** | a schema change must land before anything serves it, and drift after it fails the deploy |
502
+ | `backfill` | `run --rm`, **last** | a slow `UPDATE` inside a release gate holds the deploy open against a database still serving the *previous* release — so the sweeps are enqueued after the new pods serve, and the workers already draining the queue perform them |
503
+
504
+ Backfills are therefore **never** wired into `runMigrations()`. The `backfill` container runs `x db backfill --all --write --json` and exits.
505
+
506
+ Position in the plan is **necessary and not sufficient**: `docker compose up -d` returns when a container has started, not when the app inside it serves. The barrier that makes "after" true is declarative — the `backfill` service carries `depends_on: { web: { condition: service_healthy } }`, which `docker compose run` honours. `As of 2026-08` that service ships with the barrier in both committed compose files: [`docker/docker-compose.prod.yml`](https://github.com/developerz-ai/ultimate/blob/main/docker/docker-compose.prod.yml) and the one `x new` scaffolds. In the scaffolded app the condition is satisfied by the image's own `HEALTHCHECK` on `/healthz`, so an app that removes it also removes the barrier.
507
+
508
+ ### `x db recount` — a counter, repaired
509
+
510
+ `x db recount <entity>.<column>` compares a `countedIn` counter with the count of child rows and lists the drifted rows; `--write` sets them in one cross-tenant transaction. A counter drifts only while its trigger was missing (`X_LIFECYCLE_TRIGGER_MISSING`). As of 2026-11 the repair is one statement, not a paced `backfill()` pass.
511
+
512
+ ### `x g backfill` — the generator
513
+
514
+ Scaffolds a working pair, never a stub: `x g backfill <name> --feature <slice>` writes `<slice>/backfills/<name>.ts` — a `backfill()` declaration reading the feature's own table through `tableFor(entity, postgresRepo(entity))`, sweeping with `.where({ orgId })` guarded by an `assert` that the actor carries one, and writing back through `upsertAll` — plus `<name>.test.ts` asserting the declaration (name, retry, idempotency key), the manifest projection, and that the row transform is genuinely idempotent — `expect(<name>Row(once)).toEqual(once)`, deep value equality, since a transform that returns a fresh object every pass is still idempotent and `===` would fail it — and actually enqueues/dedupes against a memory job driver. A generated no-op handler that checkpoints a page it never wrote would report rows swept that nobody touched, which is why the template ships a real (if trivial) row transform rather than a `throw new Error(…)`.
515
+
516
+ ## Errors
517
+
518
+ | Code | Cause | Fix |
519
+ |---|---|---|
520
+ | `X_MIGRATION_CONFLICT` | the ledger disagrees with this build — a foreign app-version row that is not a rollback (rows all newer than the build are accepted), or an applied migration whose checksum moved | deploy the version `cause` names, or `x db gen "fix <migration>"` — never edit an applied migration |
521
+ | `X_MIGRATION_IRREVERSIBLE` | a generated plan drops a column or table and its `down` cannot restore the rows | `x db gen "<name>" --allow-destructive`, or put the column back in `columns` and keep it |
522
+ | `X_MIGRATION_DESTRUCTIVE` | a committed migration's `up` destroys data with no `-- destructive: true` line | add the marker, or regenerate with `--allow-destructive` — before the migration is applied |
523
+ | `X_MIGRATION_SNAPSHOT_MISSING` | the newest migration on disk carries no `.snapshot.json` sidecar to diff against | `git checkout -- packages/db/migrations/<id>.snapshot.json` — or, when it was never written, `rm packages/db/migrations/<id>.* && x db gen "<name>"`, the delete first |
524
+ | `X_MIGRATION_CONCURRENT_IN_TRANSACTION` | a migration without `-- transaction: false` holds a concurrent index statement, which Postgres refuses inside a transaction block (`25001`) | `sed -i '1i -- transaction: false' packages/db/migrations/<id>.sql` before it is applied, or move the statement into a migration of its own. Refused before `BEGIN`, so nothing is applied |
525
+ | `X_MIGRATION_VIEW_DEPENDS` | a view is compiled against a column this migration retypes — Postgres answers `0A000` and rolls the whole migration back | `drop view "<v>";` then `x db migrate`, then re-create it from the `create view` the `fix` line carries verbatim. Caught by a **preflight** inside the migration's own transaction, before its first statement, so nothing partial is applied |
526
+ | `X_DB_DRIFT` | the live schema, or the source, disagrees with the migrations | `x db gen "<name>"` — see [Entities and migrations → Drift is a `x verify` failure](Entities-And-Migrations#drift-is-a-x-verify-failure) |
527
+ | `X_SCHEMA_DUMP_DRIFT` | `packages/db/schema/` is not what the migrations produce, or was never generated | `x db gen` — see [The schema dump](#the-schema-dump) |
528
+
529
+ Seven codes, and each one exists because it sends the reader somewhere different — run it, force it, change environment, migrate first, wait, fix the predicates, fix the name. All are `@ultimat3/jobs`': the CLI throws the framework package's errors rather than minting a parallel set, and the pass enforces two of them itself.
530
+
531
+ | Code | Cause | Fix |
532
+ |---|---|---|
533
+ | `X_BACKFILL_PENDING` | declared, and `x_backfills` holds no completed pass for it in this environment | `x db backfill <name> --write --json` |
534
+ | `X_BACKFILL_APPLIED` | the ledger already records a completed pass and `--force` was not given | `x db backfill <name> --write --force --json` — a rerun is a NEW row, never an edit |
535
+ | `X_BACKFILL_ENVIRONMENT` | `environments` does not include the one this process resolved | run it where `ULTIMATE_ENV` matches, or add this environment to the declaration |
536
+ | `X_BACKFILL_MIGRATION_PENDING` | `requires` names a migration `x_migrations` does not record as applied | `x db migrate --json` |
537
+ | `X_BACKFILL_RUNNING` | the enqueue deduped — one live pass per name, and this name already has one | `x jobs show <id> --json`; a pass that is not advancing is a worker that lost its lease |
538
+ | `X_BACKFILL_STALLED` | the source is exhausted and `count()` still matches rows — the two predicates disagree | make `count()` select on exactly what `source()` selects on |
539
+ | `X_BACKFILL_UNKNOWN` | no declaration in this app carries that name | `x db backfill --pending --json` lists every declared name |
540
+
541
+ `X_BACKFILL_WRITE_UNCONFIRMED` was considered and **rejected**: a dry run that wrote nothing did exactly what it was asked to, and a code whose fix line duplicates another one's is code inflation.
542
+
543
+ A bad `batch`/`rate` throws `@ultimat3/core`'s `assert`, which is `X_INVARIANT` — borrowed, not declared, and it carries its own cause and fix at the declaration site:
544
+
545
+ | Code | Cause | Fix |
546
+ |---|---|---|
547
+ | `X_INVARIANT` (bad `batch`) | `backfill "<name>" declares batch: <n> — a batch is a whole number of rows, at least one` | `set batch: 1000 on backfill("<name>") — the rows one statement reads and one durable step handles` |
548
+ | `X_INVARIANT` (bad `rate`) | `backfill "<name>" declares rate: <n> — a rate is batches per second, greater than zero` | `set rate: 0.5 on backfill("<name>"), or leave it out — to sweep faster raise the number, there is no unthrottled mode` |
549
+
550
+ Both throw at import, not at the first batch, so a bad number is a failed build rather than a dead-lettered job. Everything past that runs the same retry/dead-letter path as any other `job` ([Jobs and workflows → Errors](Jobs-And-Workflows)).
551
+
552
+ Full list with `--json` shapes: [Error codes](Error-Codes). Migration workflow, drift and the reversible/destructive distinction in full: [Entities and migrations](Entities-And-Migrations). The `job` primitive, steps, retries and the outbox: [Jobs and workflows](Jobs-And-Workflows).