@voltro/cli 0.33.0 → 0.34.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 (337) hide show
  1. package/CHANGELOG.md +1801 -0
  2. package/bin/nodeEnvironment.d.mts +30 -0
  3. package/bin/nodeEnvironment.mjs +158 -0
  4. package/bin/voltro.mjs +69 -5
  5. package/dist/addCommand-BNeoeSxe.js +124 -0
  6. package/dist/addCommand-aXSQveak.js +2 -0
  7. package/dist/agentsMd-BTchIZku.js +2 -0
  8. package/dist/agentsMd-mhQMF1bx.js +254 -0
  9. package/dist/apiBuild-BICVGnEC.js +2 -0
  10. package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DZF_f0_I.js} +46 -46
  11. package/dist/appPort-B_HpJ_ck.js +48 -0
  12. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  13. package/dist/baselineCommand-DIttzO8A.js +227 -0
  14. package/dist/bin.js +71 -28
  15. package/dist/build-CE7Ca9gm.js +711 -0
  16. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  17. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  18. package/dist/checkCommand-Dv8r9tGO.js +231 -0
  19. package/dist/checkCommand-YZDmkAGd.js +2 -0
  20. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  21. package/dist/cliError-BmdYnghb.js +10 -0
  22. package/dist/cliOutput-D1tSBoRM.js +15 -0
  23. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  24. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  25. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  26. package/dist/clusterCmd-CNXKlcvD.js +54 -0
  27. package/dist/codegen-CYM3Zqrf.js +605 -0
  28. package/dist/codegen-ChBi_hVa.js +2 -0
  29. package/dist/codegenCommand-DC6w2tNZ.js +30 -0
  30. package/dist/codemodRunner-DRRqXR74.js +5243 -0
  31. package/dist/commandRunner-BLAEFLjp.js +47 -0
  32. package/dist/commands-gutsz-Ac.js +808 -0
  33. package/dist/connectionConfig-UFlIEiys.js +66 -0
  34. package/dist/dashboardCommand-3YG8p-UA.js +25 -0
  35. package/dist/dataCommand-qL0r7fPO.js +535 -0
  36. package/dist/dataProfile-dW-PsfLB.js +15 -0
  37. package/dist/{dbCommand-DTLKAfbA.js → dbCommand-B6X0FZbq.js} +551 -401
  38. package/dist/dbCommand-CpdKLeQq.js +2 -0
  39. package/dist/{dev-C_P8FLSx.js → dev-CerMd0mW.js} +3122 -3129
  40. package/dist/dev-CoG-ZPx8.js +3 -0
  41. package/dist/devActivity-Dx_3nnGv.js +100 -0
  42. package/dist/devActivity.js +1 -1
  43. package/dist/dialectDriver-CgXnDfec.js +39 -0
  44. package/dist/discover-C9XKJDco.js +25 -0
  45. package/dist/doctorCommand-BvqGBwNG.js +2 -0
  46. package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-CnDXQxfa.js} +520 -1278
  47. package/dist/dormancyCommand-Dszo57d6.js +69 -0
  48. package/dist/e2eCmd-BRabZww-.js +147 -0
  49. package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
  50. package/dist/envCommand-DPgdV1Bq.js +60 -0
  51. package/dist/evalCommand-6RUfPen4.js +118 -0
  52. package/dist/evolveCommand-DHpkgjgH.js +281 -0
  53. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  54. package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
  55. package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
  56. package/dist/generateCommand-oibemh97.js +147 -0
  57. package/dist/index.d.ts +45 -0
  58. package/dist/index.js +4 -3
  59. package/dist/infoCommand-BJw9nLUR.js +60 -0
  60. package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
  61. package/dist/inspect-C_T_WGvl.js +2 -0
  62. package/dist/inspectCmd-Bppy-GGw.js +224 -0
  63. package/dist/inspectFetch-Cm8_wVvp.js +151 -0
  64. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  65. package/dist/loadEnv-D9nEOClM.js +44 -0
  66. package/dist/logFileSink-C_D2wRN1.js +105 -0
  67. package/dist/logsCmd-CCca3KRZ.js +260 -0
  68. package/dist/manifestBuild-ChsKAhmn.js +2 -0
  69. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
  70. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  71. package/dist/migrate-CBwOt_iV.js +83 -0
  72. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  73. package/dist/newCommand-COWOJ1_E.js +156 -0
  74. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  75. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  76. package/dist/pageConvention-cEiRxdab.js +5 -0
  77. package/dist/privacyCommand-C-Df56U_.js +146 -0
  78. package/dist/projectScaffold-DmzEKHib.js +2 -0
  79. package/dist/projectScaffold-LMMtaavR.js +814 -0
  80. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  81. package/dist/renderProfile-1OWWAAtx.js +81 -0
  82. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  83. package/dist/runtimeTrace-CRxalXTs.js +91 -0
  84. package/dist/scheduleCmd--jksTrf6.js +69 -0
  85. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  86. package/dist/schemaIr-UJybUUZW.js +103 -0
  87. package/dist/{sdkgen-C81QIkiL.js → sdkgen-CYJscZC7.js} +111 -209
  88. package/dist/seedRunner-TFHHiToI.js +329 -0
  89. package/dist/serveCommand-B_isw7q4.js +1647 -0
  90. package/dist/serveCommand-DOvbgRnQ.js +2 -0
  91. package/dist/serveEntry.js +5 -5
  92. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  93. package/dist/start-9LiUOfES.js +1087 -0
  94. package/dist/start-B-9Nsp-S.js +3 -0
  95. package/dist/startEntry.js +2 -2
  96. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  97. package/dist/storageCommand-Co6NfLqN.js +42 -0
  98. package/dist/templates-De8IR5-c.js +102 -0
  99. package/dist/test-CI6iDsYc.js +115 -0
  100. package/dist/tracesCmd-CkEZQrtt.js +232 -0
  101. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  102. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  103. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  104. package/dist/updateCommand-Bkptutss.js +585 -0
  105. package/dist/updateCommand-us1_hdIC.js +2 -0
  106. package/dist/{inspectMetrics-1xzTKAFx.js → webDev-CBYvPqQr.js} +975 -1559
  107. package/dist/webDev-Cg-fFiyd2.js +2 -0
  108. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  109. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  110. package/dist/webhooksCommand-CID96Rga.js +267 -0
  111. package/dist/workflowsCmd-D1VTmLMY.js +608 -0
  112. package/package.json +179 -17
  113. package/templates/AGENTS.core.md +58 -3
  114. package/templates/AGENTS.md +64 -7
  115. package/templates/agent-docs/_index.md +6 -4
  116. package/templates/agent-docs/_manifest.json +22 -5
  117. package/templates/agent-docs/ai.md +370 -0
  118. package/templates/agent-docs/authentication.md +265 -31
  119. package/templates/agent-docs/caching.md +6 -0
  120. package/templates/agent-docs/cli.md +794 -50
  121. package/templates/agent-docs/data.md +550 -11
  122. package/templates/agent-docs/database/migrations.md +174 -25
  123. package/templates/agent-docs/database/misc.md +156 -40
  124. package/templates/agent-docs/database/querying.md +19 -1
  125. package/templates/agent-docs/database/scaling.md +60 -0
  126. package/templates/agent-docs/database/schema.md +1 -0
  127. package/templates/agent-docs/database/seedsdialects.md +208 -19
  128. package/templates/agent-docs/database/transactions.md +68 -0
  129. package/templates/agent-docs/deployment.md +69 -3
  130. package/templates/agent-docs/introduction.md +87 -16
  131. package/templates/agent-docs/local-first-mobile.md +79 -4
  132. package/templates/agent-docs/multi-tenancy.md +95 -20
  133. package/templates/agent-docs/observability.md +58 -3
  134. package/templates/agent-docs/plugins/ai-flows.md +161 -2
  135. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  136. package/templates/agent-docs/plugins/audit.md +37 -1
  137. package/templates/agent-docs/plugins/auth-social.md +143 -0
  138. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  139. package/templates/agent-docs/plugins/auth.md +131 -6
  140. package/templates/agent-docs/plugins/billing.md +132 -15
  141. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  142. package/templates/agent-docs/plugins/clickhouse.md +1 -1
  143. package/templates/agent-docs/plugins/duckdb.md +1 -1
  144. package/templates/agent-docs/plugins/flags.md +132 -0
  145. package/templates/agent-docs/plugins/governance.md +105 -7
  146. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  147. package/templates/agent-docs/plugins/presence.md +13 -2
  148. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  149. package/templates/agent-docs/plugins/search.md +157 -6
  150. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  151. package/templates/agent-docs/plugins/webhooks.md +105 -0
  152. package/templates/agent-docs/plugins.md +150 -18
  153. package/templates/agent-docs/reference.md +60 -3
  154. package/templates/agent-docs/releases.md +1117 -0
  155. package/templates/agent-docs/routing.md +43 -25
  156. package/templates/agent-docs/scheduling.md +10 -0
  157. package/templates/agent-docs/schema-driven-ui.md +92 -12
  158. package/templates/agent-docs/security.md +426 -0
  159. package/templates/agent-docs/templates/apibackends.md +87 -18
  160. package/templates/agent-docs/templates/appshells.md +32 -14
  161. package/templates/agent-docs/templates/overview.md +13 -8
  162. package/templates/agent-docs/testing.md +211 -14
  163. package/templates/agent-docs/whats-new.md +1700 -104
  164. package/templates/agent-docs/workflows.md +130 -14
  165. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  166. package/templates/apps/api-ai/package.json +8 -7
  167. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  168. package/templates/apps/api-auth/package.json +9 -8
  169. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  170. package/templates/apps/api-backend/package.json +12 -8
  171. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  172. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  173. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  174. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  175. package/templates/apps/api-backend-deactivation/package.json +8 -7
  176. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  177. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  178. package/templates/apps/api-backend-mail/package.json +9 -8
  179. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  180. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  181. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  182. package/templates/apps/api-backend-mariadb/package.json +10 -9
  183. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  184. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  185. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  186. package/templates/apps/api-backend-sqlite/package.json +9 -8
  187. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  188. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  189. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  190. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  191. package/templates/apps/api-backend-storage/package.json +9 -8
  192. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  193. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  194. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  195. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  196. package/templates/apps/api-cms/app.config.ts +19 -0
  197. package/templates/apps/api-cms/authz.ts +63 -0
  198. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  199. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  200. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  201. package/templates/apps/api-cms/package.json +11 -10
  202. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  203. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  204. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  205. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  206. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  207. package/templates/apps/api-collab/package.json +9 -8
  208. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  209. package/templates/apps/api-data-advanced/package.json +9 -8
  210. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  211. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  212. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  213. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  214. package/templates/apps/api-durable/package.json +9 -8
  215. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  216. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  217. package/templates/apps/api-feature-flags/package.json +10 -9
  218. package/templates/apps/api-governance/README.md +30 -8
  219. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  220. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  221. package/templates/apps/api-governance/database/schema.ts +16 -4
  222. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  223. package/templates/apps/api-governance/package.json +9 -8
  224. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  225. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  226. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  227. package/templates/apps/api-kv/package.json +9 -8
  228. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  229. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  230. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  231. package/templates/apps/api-moderation/package.json +9 -8
  232. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  233. package/templates/apps/api-observability/package.json +9 -8
  234. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  235. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  236. package/templates/apps/api-ratelimit/package.json +9 -8
  237. package/templates/apps/api-rbac/package.json +9 -8
  238. package/templates/apps/api-rest/package.json +8 -7
  239. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  240. package/templates/apps/api-saas/package.json +12 -11
  241. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  242. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  243. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  244. package/templates/apps/api-saas-starter/authz.ts +75 -0
  245. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  246. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  247. package/templates/apps/api-saas-starter/package.json +15 -11
  248. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  249. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  250. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  251. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  252. package/templates/apps/api-search/package.json +9 -8
  253. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  254. package/templates/apps/api-status/README.md +10 -3
  255. package/templates/apps/api-status/app.config.ts +8 -3
  256. package/templates/apps/api-status/authz.ts +5 -3
  257. package/templates/apps/api-status/package.json +9 -8
  258. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  259. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  260. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  261. package/templates/apps/api-status/tests/status.test.ts +9 -1
  262. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  263. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  264. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  265. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  266. package/templates/apps/api-versioning/package.json +9 -8
  267. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  268. package/templates/apps/api-webhooks/package.json +10 -9
  269. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  270. package/templates/apps/changelog/package.json +8 -6
  271. package/templates/apps/edge-functions/package.json +2 -2
  272. package/templates/apps/frontend-admin/package.json +10 -8
  273. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  274. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  275. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  276. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  277. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  278. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  279. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  280. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  281. package/templates/apps/frontend-app/app.config.ts +4 -3
  282. package/templates/apps/frontend-app/package.json +11 -8
  283. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  284. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  285. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  286. package/templates/apps/frontend-auth/package.json +10 -8
  287. package/templates/apps/frontend-blank/package.json +9 -7
  288. package/templates/apps/frontend-cms/package.json +11 -9
  289. package/templates/apps/frontend-collab/package.json +12 -9
  290. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  291. package/templates/apps/frontend-contact/package.json +9 -7
  292. package/templates/apps/frontend-dashboard/package.json +9 -7
  293. package/templates/apps/frontend-docs/package.json +9 -7
  294. package/templates/apps/frontend-i18n/package.json +8 -6
  295. package/templates/apps/frontend-landing/package.json +9 -7
  296. package/templates/apps/frontend-portal/package.json +10 -8
  297. package/templates/apps/frontend-saas/app.config.ts +10 -6
  298. package/templates/apps/frontend-saas/package.json +10 -8
  299. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  300. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  301. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  302. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  303. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  304. package/templates/apps/frontend-spa/package.json +9 -7
  305. package/templates/apps/frontend-ssr/package.json +9 -7
  306. package/templates/apps/frontend-ssr-api/package.json +10 -8
  307. package/templates/apps/frontend-static-blog/package.json +8 -6
  308. package/templates/apps/frontend-status/package.json +10 -8
  309. package/templates/apps/mobile-app/README.md +1 -0
  310. package/templates/apps/mobile-app/package.json +4 -2
  311. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  312. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  313. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  314. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  315. package/templates/baselines/bare/.env.example +14 -0
  316. package/templates/baselines/bare/baseline.json +4 -4
  317. package/templates/baselines/compose/.env.example +14 -0
  318. package/templates/baselines/compose/README.md +1 -1
  319. package/templates/baselines/compose/baseline.json +5 -5
  320. package/templates/baselines/compose-mariadb/.env.example +14 -0
  321. package/templates/baselines/compose-mariadb/README.md +1 -1
  322. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  323. package/templates/baselines/helm/.env.example +14 -0
  324. package/templates/baselines/helm/baseline.json +4 -4
  325. package/dist/apiBuild-C-x9YacA.js +0 -2
  326. package/dist/checkCommand-DRovTKza.js +0 -2
  327. package/dist/commands-CJfepbm4.js +0 -11541
  328. package/dist/dbCommand-b1gum4td.js +0 -2
  329. package/dist/dev-iiMtlkfs.js +0 -3
  330. package/dist/devActivity-BhIu6ncs.js +0 -159
  331. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  332. package/dist/inspect-mmBuRXmy.js +0 -2
  333. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  334. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  335. package/dist/serveCommand-DdaM4Hup.js +0 -1608
  336. package/dist/start-C0koT0UO.js +0 -1084
  337. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -17,7 +17,7 @@ Voltro's migrator does not generate or apply migration files. Instead, on every
17
17
  2. **Diffs** that against your declared schema (every `*.entity.ts` / `*.schema.ts` file in the project plus the framework's bookkeeping tables).
18
18
  3. **Classifies** each pending DDL op into one of seven `OperationClass`es — `safe`, `needs-default`, `needs-backfill`, `needs-rename-annotation`, `lossy`, `online-required`, `multi-step`.
19
19
  4. **Refuses** to apply anything that can't be made safe automatically. The diff output tells you exactly which DSL annotation to add (`.backfill()`, `.renamedFrom()`, `dropped()`, …).
20
- 5. **Applies** the rest under an advisory lock + records the result in `_voltro_migration_plans` with a fingerprint of the post-apply schema.
20
+ 5. **Applies** the rest under an advisory lock + records the result in `_voltro_migration_plans` with a fingerprint of the post-apply schema. On a dialect that cannot apply the whole plan in one transaction (MySQL / MariaDB / SQLite / Turso), each operation is also recorded in a **resume ledger** (`_voltro_migration_ops`) as it lands, so a crashed apply is continued rather than re-planned blind — see [multi-dialect](./multi-dialect.md).
21
21
 
22
22
  There are no generated SQL files to commit, no `migrations/` directory to rebase, no checksum manifest to repair. The source of truth is your schema TypeScript; the DB is the slave.
23
23
 
@@ -862,28 +862,32 @@ completed rows.
862
862
  <!-- source: en/database/migrations/multi-dialect.md -->
863
863
  ## Multi-dialect strategy
864
864
 
865
- _How the planner + applier behave across Postgres, MySQL, MariaDB, MSSQL, SQLite — the atomicity matrix, MySQL's implicit-commit landmine + re-diff-to-recover, SQLite's table-rewrite mechanic, plus the per-dialect DDL idioms the framework hides._
865
+ _How the planner + applier behave across Postgres, MySQL, MariaDB, MSSQL, SQLite, Turso — the atomicity matrix, the resume ledger that carries a crashed apply on the non-transactional dialects, SQLite's table-rewrite mechanic, plus the per-dialect DDL idioms the framework hides._
866
866
 
867
867
  The planner produces ONE `MigrationPlan` regardless of dialect. The applier executes it per-dialect, dispatching through `sql.onDialectOrElse` for every emit + falling back to runtime probes when behaviour diverges. The same `voltro db apply` invocation against the same schema produces structurally identical results on every backend.
868
868
 
869
869
  What ISN'T uniform: **transactional DDL semantics**.
870
870
 
871
- | Dialect | Transactional DDL | Advisory-lock mechanism | Backfill in tx with DDL |
871
+ | Dialect | Plan applied atomically | Advisory-lock mechanism | Recovery after a mid-plan crash |
872
872
  |---|---|---|---|
873
- | Postgres | ✓ all DDL atomic | `pg_advisory_lock(KEY)` | |
874
- | MSSQL | ✓ all DDL atomic | `sp_getapplock` | |
875
- | SQLite | all DDL atomic | process-local mutex | |
876
- | **MySQL / MariaDB** | **✗ implicit commit per DDL** | `GET_LOCK('voltro_migration', N)` | but SEPARATE from DDL |
873
+ | Postgres | ✓ one transaction | `pg_advisory_lock(KEY)` | nothing to recover — rolled back |
874
+ | MSSQL | ✓ one transaction | `sp_getapplock` | nothing to recover — rolled back |
875
+ | **MySQL / MariaDB** | **✗ implicit commit per DDL** | `GET_LOCK('voltro_migration', N)` | resume ledger |
876
+ | **SQLite / Turso** | **✗ per statement** | process-local mutex | resume ledger |
877
877
 
878
878
  This is the operationally heaviest cross-dialect difference. The rest of the page covers what changes.
879
879
 
880
- ## Postgres / MSSQL / SQLite transactional happy path
880
+ **SQLite is on the non-atomic side, and the reason is Turso.** SQLite the engine *can* do transactional DDL. The applier does not use it, because SQLite and Turso share one dialect token and Turso rejects DDL inside its default transaction — the applier cannot wrap one without wrapping the other. Both therefore take the per-statement path and the resume ledger below.
881
+
882
+ **On Postgres one class of operation is still not covered by the transaction:** `online-required` ops (`CREATE INDEX CONCURRENTLY`, the shadow-column type swap) are *rejected* inside a transaction, so they run after the commit. They are ledgered like a MySQL plan.
883
+
884
+ ## Postgres / MSSQL — transactional happy path
881
885
 
882
886
  A multi-step plan runs inside one `BEGIN ... COMMIT`. Mid-flight failure rolls EVERYTHING back; the next plan diff is identical to the pre-apply one. There's nothing to resume — re-running the apply re-runs the plan from scratch.
883
887
 
884
888
  The advisory-lock variants serialise concurrent applies — two operators running `voltro db apply` against the same DB at the same time go through serially.
885
889
 
886
- ## MySQL / MariaDB — re-diff-to-recover territory
890
+ ## MySQL / MariaDB / SQLite / Turso the resume ledger
887
891
 
888
892
  Every DDL statement implicitly commits. A 5-op plan on MySQL is effectively 5 separate "atomic statements" with the prior ones already committed when a later one fails. If op 5 of 5 fails, ops 1–4 stay applied:
889
893
 
@@ -896,13 +900,36 @@ plan applying (mysql, env=prod):
896
900
  5. add-index audit_logs(actorId) ✗ ER_DUP_KEYNAME
897
901
  ```
898
902
 
899
- There is NO `--resume` / `--abort` flag and NO per-op partial-status rowthe applier records a `_voltro_migration_plans` row only on a fully successful apply. Recovery is to re-run the apply once the cause is fixed:
903
+ The `_voltro_migration_plans` row is still written only on a fully successful applythat row means "this schema is live", and a half-applied plan is not. What the applier DOES write as it goes is a per-operation **resume ledger**, `_voltro_migration_ops`:
904
+
905
+ - every operation that will run outside a transaction is inserted `pending` **before any DDL runs**, so a crash on operation 1 still leaves the whole intended sequence on disk;
906
+ - each row flips to `started` immediately before its statement and `applied` immediately after;
907
+ - the rows are **deleted** once the apply converges and the plan row lands. The ledger is a work queue, not a history — the history is `_voltro_migration_plans.operations`.
908
+
909
+ There is still no `--resume` / `--abort` flag, because there is nothing to choose. Recovery is to re-run the apply once the cause is fixed:
900
910
 
901
911
  ```sh
902
912
  voltro db apply
903
913
  ```
904
914
 
905
- `voltro db apply` re-introspects the live DB and diffs the declared schema against the (half-applied) live shape, so it emits ONLY the ops still missing — ops 1–4 are already in the DB and don't reappear in the diff. If someone finished op 5 out of band (psql, a corrective hot-fix), the re-diff sees it as present and skips it too. If the failed op is no longer the right answer because the schema was edited in response, the next `voltro db plan` already reflects the new declared shape — nothing to abort.
915
+ The next apply finds the ledger, says so in the log, and continues the interrupted run:
916
+
917
+ ```
918
+ [voltro:migrate] migration resume: found an interrupted run (plan_msocz71h_x0qq5d) — 4 of 5 operation(s) completed, in flight: add-index index:audit_logs.audit_logs_actorId_idx
919
+ [voltro:migrate] migration resume: continuing run plan_msocz71h_x0qq5d — replaying 1 operation(s), 4 already applied
920
+ ```
921
+
922
+ Three things are worth knowing about how it decides.
923
+
924
+ **The ledger cannot be atomic with the DDL it records** — on MySQL the statement commits itself, so there is always a window where the DDL landed and the `applied` flip did not. That window is not eliminated, it is BOUNDED: the ledger is written strictly sequentially, so **at most one operation can be `started`**, and it is the only one whose outcome is unknown. That one is resolved by asking the planner — the fresh diff was computed against the live database moments ago, so an operation it no longer mentions has already taken effect. Completed operations are never re-attempted.
925
+
926
+ **If you edited the schema in response to the failure, the recorded plan is dropped** and the freshly-diffed one is applied instead — the old plan aims at a target nobody wants any more. The interruption is still logged, and the artefacts below are still reconciled first.
927
+
928
+ **Two operations are repaired rather than re-diffed.** The Postgres online type change (shadow-column swap) and the SQLite table rebuild both build a temporary object and swap it into place, and interrupted mid-swap they leave a live schema that means something ELSE to a differ — a half-finished shadow swap looks like a *missing column*, and the plan a blind re-diff produces for that is `add-column`, which succeeds and loses the data sitting in `<col>__old`. The applier reconciles `<col>__shadow` / `<col>__old` and `<table>__voltro_rebuild` from their observable state before anything else reads the schema. If a shadow-swap state cannot be classified, the apply **refuses**, changes nothing, and names the three columns to inspect.
929
+
930
+ **What has not changed: convergence still gates the fingerprint.** After the DDL — resumed or not — the applier re-plans against the live schema and refuses to record a fingerprint while anything remains. A resumed run is held to exactly the same standard as a fresh one, and an apply that does not converge KEEPS its ledger, because an unfinished run's record is the only thing that tells the next boot it is looking at a half-migrated schema.
931
+
932
+ If someone finished the failed op out of band (a `mysql` shell, a corrective hot-fix), the fresh diff sees it as present and it is skipped.
906
933
 
907
934
  ## SQLite — table rewrite mechanic
908
935
 
@@ -954,7 +981,7 @@ The DDL emitter under `@voltro/database/src/migrate.ts` is one of the densest cr
954
981
  [voltro:dev] auto-migrate: applied 3 op(s) in 412ms [safe=3 needs-default=0 needs-backfill=0 rename=0 lossy=0] fingerprint=8f507ba1e1aadad5
955
982
  ```
956
983
 
957
- For MySQL the line notes the non-atomic-DDL constraint (a failed op leaves earlier ops committed; re-run apply to finish):
984
+ For MySQL the line notes the non-atomic-DDL constraint (a failed op leaves earlier ops committed; re-run apply and the resume ledger continues from there):
958
985
 
959
986
  ```
960
987
  [voltro:dev] auto-migrate: planning schema dialect=mysql env=dev tables=22 (implicit-commit DDL — re-run apply after a mid-plan failure)
@@ -1302,6 +1329,36 @@ If `down` throws, the rollback is treated as failed — the schema stays
1302
1329
  in the half-rolled-back state + the operator handles it manually. The
1303
1330
  framework can't auto-recover from a broken inverse.
1304
1331
 
1332
+ ## `voltro serve` refuses to boot while any are pending
1333
+
1334
+ Serve's schema guard is a DECLARATIVE fingerprint diff — the declared schema
1335
+ against the last applied plan. A file-based migration exists for the changes a
1336
+ state diff cannot infer: a data move, a backfill, a cross-table rewrite. Those
1337
+ move **no fingerprint at all**, so the guard passed and production ran
1338
+ un-migrated with nothing said.
1339
+
1340
+ On a real deploy environment (`NODE_ENV=production` / `staging`), against a SQL
1341
+ store, `voltro serve` now refuses to boot while any migration file has never run
1342
+ against that database, and names the pending ids:
1343
+
1344
+ ```text
1345
+ serve: refusing to boot — 2 pending file-based migration(s) have never run
1346
+ against this database. They perform the changes a schema diff cannot infer
1347
+ (data moves, backfills, table splits), so the declarative fingerprint check
1348
+ below cannot see them.
1349
+
1350
+ Run them from your pre-deploy job — `voltro db migrate .` (schema + files)
1351
+ or `voltro db files .` (files alone).
1352
+ ```
1353
+
1354
+ It does **not** apply them, and that is deliberate: a rolling deploy starts N
1355
+ replicas, each would try, and the migration lock turns that into N-1 processes
1356
+ blocked on boot. `VOLTRO_AUTO_MIGRATE=0` bypasses this exactly as it already
1357
+ bypassed the fingerprint check — one switch for "no boot-time schema checks".
1358
+
1359
+ A local `voltro serve` is untouched: `voltro dev` applies migrations there, so a
1360
+ preview serve has nothing to report.
1361
+
1305
1362
  ## Remote databases: boot will not apply them unattended
1306
1363
 
1307
1364
  `voltro dev` applies pending migration files at boot. Against a local
@@ -1521,9 +1578,25 @@ The applier:
1521
1578
  1. Introspects the live DB and plans the diff fresh (it does NOT ingest
1522
1579
  a plan file — the diff is computed against live at apply time)
1523
1580
  2. Refuses (exit 2) if any op is blocked, or refuses (exit 3) if
1524
- `NODE_ENV=production` — so the apply runs in a one-shot job with
1525
- `NODE_ENV` unset / `staging`, holding migration credentials, NOT in
1526
- the serving process
1581
+ `NODE_ENV=production` — so a bare apply runs in a one-shot job with
1582
+ `NODE_ENV=staging`, holding migration credentials, NOT in the serving
1583
+ process
1584
+
1585
+ > **`NODE_ENV` unset is no longer "not production".** Every `voltro db …`
1586
+ > and `voltro migrate` invocation resolves an unset `NODE_ENV` to
1587
+ > `production`, exactly as `voltro serve` and `voltro start` do — so a bare
1588
+ > `voltro db apply` in a pipeline that forgot the variable now refuses (exit
1589
+ > 3) instead of silently applying an un-reviewed diff to production. Set
1590
+ > `NODE_ENV=development` for a local database; use the `--plan` path below
1591
+ > for a real one.
1592
+ >
1593
+ > This is not only about the refusal. `_voltro_traces` and `_voltro_undo_log`
1594
+ > are created only when tracing / undo capture are on, and both are *on
1595
+ > unless production* — so an apply with `NODE_ENV` unset used to DECLARE two
1596
+ > tables the serving container did not. The declared set is what the schema
1597
+ > fingerprint hashes, so the apply recorded a fingerprint the container could
1598
+ > not reproduce and `voltro serve` refused to boot with `prod-mismatch`,
1599
+ > telling you to run the apply you had just run.
1527
1600
  3. Acquires the advisory lock + executes the plan
1528
1601
  4. Records the result in `_voltro_migration_plans` with
1529
1602
  `source: 'auto-diff'` + `notes: 'PR #1234 — add user emails'`
@@ -1554,6 +1627,31 @@ up-to-date DB is a clean no-op (`schema is up to date — nothing to
1554
1627
  apply`). That's what makes the apply safe to run in every pod of a
1555
1628
  stateless deploy.
1556
1629
 
1630
+ Both spellings of the flag work: `--plan plan.json` and `--plan=plan.json`.
1631
+
1632
+ ### The FIRST deploy, against an empty database
1633
+
1634
+ Nothing special is required, and the plan you review is the whole story: on a
1635
+ database with no tables, `voltro db plan --json` includes the framework's own
1636
+ `_voltro_*` tables (the migration ledger, api keys, kv, outbox, traces …) plus
1637
+ `actors`, alongside your own. They are part of the declared schema, so they are
1638
+ planned, classified and applied by exactly the same code as your tables — expect
1639
+ a first-deploy plan to be ~20 operations larger than the diff you wrote.
1640
+
1641
+ Two consequences worth knowing:
1642
+
1643
+ - The reviewed plan is complete. `db apply --plan` creates nothing beside it, so
1644
+ the fingerprint the plan was generated against is still the live schema when
1645
+ the guard checks it. (It did not used to be: the ledger tables were created
1646
+ before the fingerprint was taken, so the first deploy of every new database
1647
+ refused with `the live schema has drifted` one second after the plan was
1648
+ generated. Fixed.)
1649
+ - One table is deliberately absent from the plan: `_voltro_migration_ops`, the
1650
+ crash-resume ledger. It has to exist before the very first plan runs — the plan
1651
+ that creates everything else — so `voltro db apply` creates it itself, under the
1652
+ migration lock. A live `_voltro_*` table your schema does not declare is never
1653
+ planned for a drop, so it does not show up in the next diff either.
1654
+
1557
1655
  ## Apply timing relative to deploy
1558
1656
 
1559
1657
  Two orderings, both common:
@@ -3036,9 +3134,23 @@ voltro db apply --note 'PR #1234'
3036
3134
 
3037
3135
  4. Re-deploy. The new boot's fingerprint check passes.
3038
3136
 
3039
- Note: the boot-mismatch message itself prints
3040
- `voltro db apply --plan plan.json`, but `--plan` is not a real flag
3041
- the prod apply is a plain `voltro db apply` that recomputes the diff.
3137
+ `--plan` IS a real flag, and it is the better answer here. This note used to
3138
+ say it was not, and told you to run a plain `voltro db apply` instead which
3139
+ recomputes the diff and therefore applies something nobody reviewed. The claim
3140
+ came from a defect, not from the design: the app root was resolved as "the
3141
+ first argument that does not start with `-`", so `--plan plan.json` handed the
3142
+ plan FILE to schema discovery (`no schema files found, root: …/plan.json`)
3143
+ while `--plan=plan.json` worked. Both spellings work now.
3144
+
3145
+ Prefer the reviewed form in a pipeline:
3146
+
3147
+ ```sh
3148
+ voltro db plan --json > plan.json # review this in the PR
3149
+ voltro db apply --plan plan.json # apply exactly it, fingerprint-guarded
3150
+ ```
3151
+
3152
+ A plain `voltro db apply` stays correct for a developer machine, where the diff
3153
+ you would review is the one you just wrote.
3042
3154
 
3043
3155
  If the cause is drift (someone DDL'd prod manually), see [drift.md](./drift.md) for reconciliation paths.
3044
3156
 
@@ -3170,13 +3282,19 @@ so a failure THERE can leave the index half-built; re-apply finishes it.)
3170
3282
 
3171
3283
  **MySQL and MariaDB** (and sqlite / turso) implicit-commit every DDL
3172
3284
  statement, so THERE a plan that fails on op N leaves ops 1..N-1 committed.
3173
- There is no `--resume` / `--abort` flag and no per-op partial-status
3174
- tracking the apply records a row only on full success.
3175
-
3176
- Recovery is just to re-run the apply: `voltro db apply` re-diffs the
3177
- declared schema against the current (half-applied) live shape and emits
3178
- only the ops that are still missing. Fix the cause of the failed op
3179
- first (e.g. the `ER_DUP_KEYNAME` that stopped op N), then:
3285
+ The `_voltro_migration_plans` row is still written only on full success
3286
+ that row means "this schema is live". What the apply DOES write as it goes
3287
+ is a per-operation **resume ledger** (`_voltro_migration_ops`): every op is
3288
+ recorded before any DDL runs, flipped to `started` before its statement and
3289
+ `applied` after, and the rows are deleted once the apply converges. There is
3290
+ still no `--resume` / `--abort` flag because there is nothing to choose.
3291
+
3292
+ Recovery is just to re-run the apply. It finds the ledger, logs
3293
+ `migration resume: found an interrupted run …`, reconciles any half-finished
3294
+ shadow-column swap or table rebuild, skips the ops that already took effect,
3295
+ and replays the rest — including a `.backfill()` that was only partly done,
3296
+ which a plain re-diff cannot express. Fix the cause of the failed op first
3297
+ (e.g. the `ER_DUP_KEYNAME` that stopped op N), then:
3180
3298
 
3181
3299
  ```sh
3182
3300
  voltro db apply --note 'completing partial apply after fixing op N'
@@ -3361,6 +3479,37 @@ db apply: change triggers converged (1501 statement(s))
3361
3479
 
3362
3480
  The mirror case is reported the same way: a `.nonReactive()` table that still carries a trigger keeps paying `REPLICA IDENTITY FULL` and a `NOTIFY` on every write for a subscription nobody receives. `db apply` removes both.
3363
3481
 
3482
+ **Boot converges them too, on both boot paths.** `voltro dev` and `voltro serve`
3483
+ run the same check-and-repair at startup, so a schema-only restore or a
3484
+ `CDC=0` → `CDC=1` flip no longer waits for someone to notice:
3485
+
3486
+ ```text
3487
+ reactive triggers: converged at boot — installed 500, removed 0 (1501 statement(s))
3488
+ ```
3489
+
3490
+ Three things about it are worth knowing before you deploy a fleet:
3491
+
3492
+ - **It does not queue.** The repair takes the migration advisory lock with
3493
+ `pg_try_advisory_lock` and SKIPS if anything holds it, so N replicas booting
3494
+ together produce one repairing and N-1 logging `another instance … is
3495
+ converging it`. A concurrent `voltro db apply` holds the same lock, so the two
3496
+ can never run each other's DDL.
3497
+ - **It never fails a boot.** A check that cannot run warns and the process
3498
+ continues; reactivity may be degraded, and that is still better than a
3499
+ diagnostic taking the app down.
3500
+ - **It is a tunable**, `reactiveTriggers` in `app.config.ts`, default `'repair'`:
3501
+
3502
+ ```ts
3503
+ export default defineApiApp({
3504
+ store: 'postgres',
3505
+ reactiveTriggers: 'report', // 'repair' (default) · 'report' · 'off'
3506
+ })
3507
+ ```
3508
+
3509
+ `VOLTRO_REACTIVE_TRIGGERS` overrides the field. `VOLTRO_AUTO_MIGRATE=0`
3510
+ downgrades `'repair'` to `'report'` — that variable means "this boot issues no
3511
+ DDL", and it is deliberately not read as "and say nothing".
3512
+
3364
3513
  ## When the fix hint doesn't match reality
3365
3514
 
3366
3515
  The fix hints come from the planner's classification logic — they should always be actionable. If you see one that doesn't make sense given your code:
@@ -1,6 +1,6 @@
1
1
  # Database
2
2
 
3
- > Spin up an isolated branch of a tenant's dataa namespace snapshot/restore by default (dialect-agnostic, no vendor lock) or a Neon copy-on-write fast-path on Postgres then bind to it and tear it down. The basis for per-PR preview environments.
3
+ > Branch the live schema and REHEARSE your migration on it apply the plan to a throwaway copy, flag every lossy operation, prove it converges, drop the branch. Plus the branch primitive itself (namespace snapshot on Postgres, Neon copy-on-write fast-path).
4
4
 
5
5
 
6
6
 
@@ -9,73 +9,178 @@
9
9
  <!-- source: en/database/branching.md -->
10
10
  ## Data branching
11
11
 
12
- _Spin up an isolated branch of a tenant's dataa namespace snapshot/restore by default (dialect-agnostic, no vendor lock) or a Neon copy-on-write fast-path on Postgres then bind to it and tear it down. The basis for per-PR preview environments._
12
+ _Branch the live schema and REHEARSE your migration on it apply the plan to a throwaway copy, flag every lossy operation, prove it converges, drop the branch. Plus the branch primitive itself (namespace snapshot on Postgres, Neon copy-on-write fast-path)._
13
13
 
14
- Data branching creates an **isolated copy of a tenant's data** you can read and
15
- write without touching the source the basis for per-PR preview environments and
16
- safe "try a migration against real-shaped data" workflows. The default mechanism
17
- is a **namespace snapshot/restore** (works on every dialect, no vendor lock); on
18
- Postgres backed by Neon, a **copy-on-write branch** fast-path is used instead.
14
+ Data branching creates an **isolated copy of a schema** you can read, write and
15
+ migrate without touching the source. The headline use is not the branch — every
16
+ serverless-Postgres vendor sells one of those it is what Voltro can do WITH a
17
+ branch that a vendor cannot: **rehearse your pending migration on it and tell you
18
+ what it would do.**
19
+
20
+ ## `voltro db branch` — the migration rehearsal
21
+
22
+ ```bash
23
+ voltro db branch --pr 128 # branch the live schema, rehearse, report, drop it
24
+ voltro db branch --pr 128 --seed copy # …with the parent's rows copied in
25
+ voltro db branch --pr 128 --keep # leave the branch standing to poke at
26
+ voltro db branch --pr 128 --json # machine-readable, for a PR comment
27
+ ```
28
+
29
+ What it does, in order:
30
+
31
+ 1. **Branches the LIVE schema** into a throwaway namespace (`br_pr128_<app>`).
32
+ 2. **Checks fidelity** — plans the declared schema against the branch AND against
33
+ the parent, and aborts if the two disagree. A branch that is not a faithful
34
+ copy rehearses a different migration from the one you are about to run, and
35
+ saying nothing about that would be worse than not rehearsing at all.
36
+ 3. **Plans your migration** against the branch and classifies every operation.
37
+ 4. **Executes it there**, including the destructive operations.
38
+ 5. **Re-plans.** An empty re-plan is the verdict; a migration that applies and
39
+ then re-proposes itself forever is not a migration.
40
+ 6. **Drops the branch** (unless `--keep`), even when the apply failed.
41
+
42
+ ```
43
+ branch rehearsal · br_pr128_shop · mechanism namespace
44
+ branched 18 table(s), replayed 3 foreign key(s)
45
+ plan: 2 operation(s), 1 lossy, 0 refused
46
+ • add-column members [safe]
47
+ ✗ drop-column members [lossy]
48
+
49
+ ⚠ 1 operation(s) DESTROY DATA. They were executed on the branch (it is
50
+ disposable) so they are rehearsed, but production refuses them until you set
51
+ VOLTRO_DESTRUCTIVE_OK — naming the tables, not `1`.
52
+
53
+ ✓ applied on the branch, and the re-plan is EMPTY (the migration converges).
54
+ branch br_pr128_shop torn down.
55
+ ```
56
+
57
+ ### Lossy operations are executed, not skipped
58
+
59
+ Production refuses a `drop-column` until a human acknowledges it. A branch that
60
+ is about to be dropped has no such reason — and refusing there would mean the one
61
+ operation most likely to fail is the one operation never rehearsed. So the
62
+ rehearsal unblocks lossy operations on the branch, runs them, and leads the report
63
+ with every one of them. That report is the thing you paste into the PR.
64
+
65
+ Operations the planner refuses **anywhere** (`needs-rename-annotation`,
66
+ `multi-step`) are NOT executed — the plan is reported as `blocked` instead.
67
+
68
+ ### Exit codes
69
+
70
+ | code | meaning |
71
+ |---|---|
72
+ | `0` | applied on the branch and converged, nothing lossy |
73
+ | `2` | a REVIEW signal: the plan destroys data, or the planner refuses part of it |
74
+ | `1` | the rehearsal could not answer — it failed, the branch was not faithful, or the migration did not converge |
75
+
76
+ `lossy` is deliberately not an error. A `drop-column` in a PR is a normal,
77
+ intentional thing; making it exit `1` trains people to pass `--force`, and the
78
+ next real failure goes with it.
79
+
80
+ ### What `--seed` does and does not rehearse
81
+
82
+ `--seed empty` (the default) branches the SCHEMA only. That is enough for every
83
+ structural question and costs nothing. It does **not** rehearse anything
84
+ data-dependent: a `NOT NULL` meeting existing NULLs, a backfill meeting real
85
+ values, a unique constraint meeting duplicates, or the row-count threshold that
86
+ promotes an operation to `online-required`. Use `--seed copy` for those — it
87
+ copies every row, which is fast on a small database and slow on a large one.
88
+
89
+ ## Mechanisms — what actually works
90
+
91
+ Be precise here, because the vendor landscape invites over-claiming. The branch
92
+ **plan** is dialect-agnostic. The shipped **executor** is not.
93
+
94
+ | mechanism | when | status |
95
+ |---|---|---|
96
+ | `namespace` | Postgres — a schema per branch | shipped, and what `voltro db branch` uses |
97
+ | `neon-cow` | Postgres on Neon, `seed: 'copy'` | plan shipped; **executed by the cloud control plane**, not the CLI |
98
+
99
+ - **MySQL, MariaDB, SQLite and SQL Server are not supported by
100
+ `voltro db branch`.** `makeNamespaceBranchExecutor` emits `CREATE SCHEMA`,
101
+ `CREATE TABLE … (LIKE … INCLUDING ALL)` and `"`-quoted identifiers, none of
102
+ which those engines accept. The command refuses with that reason rather than
103
+ sending Postgres syntax at them. Writing your own `BranchExecutor` for another
104
+ dialect is the supported path — the plan is already portable.
105
+ - **Neon copy-on-write is a call to Neon's branch API** and needs a Neon token,
106
+ which the CLI does not hold. `voltro db branch` names the mechanism and refuses,
107
+ pointing at `--prefer namespace`. The cloud control plane executes it.
108
+ - **There is no Supabase mechanism and no template-database mechanism.** Neither
109
+ exists in the codebase.
19
110
 
20
111
  ## The plan/execute split
21
112
 
22
113
  Branching is a **plan** (what to do) + an **executor** (do it), mirroring the
23
- migration engine. The plan is pure + inspectable; the executor performs the I/O.
114
+ migration engine. The plan is pure and inspectable; the executor performs the I/O.
24
115
 
25
116
  ```ts
26
117
  import { planBranch, resolveBranchMechanism } from '@voltro/database'
27
118
 
28
- // Pick the mechanism: 'neon-cow' when seeding a copy on a Neon URL,
29
- // 'namespace' (schema-per-branch) otherwise.
30
119
  const mechanism = resolveBranchMechanism({ seed: 'copy', dbUrl: connectionString })
31
120
  const steps = planBranch({
32
121
  mechanism,
33
- branchId: 'branch_acme_pr128',
122
+ branchId: 'br_pr128_shop',
34
123
  tableNames,
35
124
  seed: 'copy',
36
- parentNamespace: 'public', // namespace mechanism: the schema to snapshot from
125
+ parentNamespace: 'public',
37
126
  })
38
- // steps: e.g. [{ kind: 'neon-branch-create', … }] or per-table namespace snapshot steps
39
127
  ```
40
128
 
41
129
  ## Provision + tear down
42
130
 
43
131
  `provisionBranch` / `teardownBranch` run a plan through an injected
44
- `BranchExecutor` (the dialect/Neon adapter). Injected so the lifecycle is
45
- unit-testable with a recording executor — no live DB:
132
+ `BranchExecutor`. Injected so the lifecycle is unit-testable with a recording
133
+ executor — no live database:
46
134
 
47
135
  ```ts
48
- import { provisionBranch, teardownBranch } from '@voltro/database'
136
+ import { provisionBranch, teardownBranch, makeNamespaceBranchExecutor } from '@voltro/database'
49
137
 
50
138
  const result = await provisionBranch(
51
- { appSlug: 'acme', prNumber: 128, seed: 'copy', tableNames, dbUrl: connectionString },
52
- executor,
139
+ {
140
+ appSlug: 'shop', prNumber: 128, seed: 'copy', tableNames,
141
+ parentNamespace: 'public',
142
+ foreignKeys, // see below — LIKE does not copy these
143
+ indexNames, // see below — LIKE renames these
144
+ dbUrl: connectionString,
145
+ },
146
+ makeNamespaceBranchExecutor({ run: (sql) => client.query(sql) }),
53
147
  )
54
- // result: { branchId, mechanism, steps, … } — for 'neon-cow' also the
55
- // branch's own connection string to bind the preview store to.
56
148
 
57
- // …run the preview against result, then:
58
149
  await teardownBranch(result.branchId, result.mechanism, executor)
59
150
  ```
60
151
 
61
- `admitBranch` gates whether a branch may be served (e.g. it exists + is ready)
62
- so a request never binds to a half-provisioned branch.
152
+ `admitBranch` enforces the storage-cost caps (a TTL and a maximum number of live
153
+ branches) before a new one is provisioned.
63
154
 
64
- ## Mechanisms
155
+ ### Two things `LIKE … INCLUDING ALL` does not carry
65
156
 
66
- | mechanism | when | how |
67
- |---|---|---|
68
- | `namespace-snapshot` | any dialect (default) | snapshot the tenant's namespace → restore into a new branch namespace |
69
- | `neon-branch` | Postgres on Neon | a copy-on-write Neon branch (instant, storage-shared) |
157
+ Both were found by pointing the rehearsal at a real Postgres and watching a
158
+ converged schema propose work. They are properties of Postgres, not of Voltro's
159
+ emission, and a branch missing either is not a copy:
160
+
161
+ - **Foreign keys are not copied.** There is no `INCLUDING` clause that copies
162
+ them. A branch without them accepts writes production rejects.
163
+ - **Index names are re-derived** from the table and columns. Measured on pg 17:
164
+ `byApiKeyTenant` came back as `_voltro_api_keys_tenantId_idx`, and
165
+ `_voltro_idempotency_scope_key_uq` as `…_scope_key_idx`. The migration planner
166
+ compares indexes by name, so every custom-named index reads as a different
167
+ index.
70
168
 
71
- The mechanism is resolved from the connection, so the same `planBranch` call
72
- produces the right steps for the deployment's database dialect-agnostic by
73
- default, with the Neon fast-path when it's available. No code path is locked to a
74
- vendor.
169
+ `provisionBranch` replays both (`foreignKeys` + `indexNames`, sourced from the
170
+ parent's introspected snapshot). `voltro db branch` does it for you, and refuses
171
+ to report anything about your migration if the branch still differs from the
172
+ parent.
173
+
174
+ ## Branch-per-PR in CI
175
+
176
+ ```yaml
177
+ - run: voltro db branch --pr ${{ github.event.number }} --json > rehearsal.json
178
+ continue-on-error: true # exit 2 is a review signal, not a build failure
179
+ - run: node scripts/comment-rehearsal.mjs rehearsal.json
180
+ ```
75
181
 
76
- > Branching copies **data**, not the schema definition — apply migrations to a
77
- > branch the same way you would to any store. Teardown is explicit; a branch
78
- > left bound holds its namespace/Neon branch until torn down.
182
+ The `--json` report carries `operations`, `lossy`, `blocked`, `residual`,
183
+ `converged` and `infidelity` everything a PR comment needs, already classified.
79
184
 
80
185
 
81
186
 
@@ -281,11 +386,22 @@ recoveryNote: text().encrypted(), // encrypted at rest, but the o
281
386
  apiToken: text().encrypted().serverOnly(), // secret at rest AND never to a client
282
387
  ```
283
388
 
284
- Enforcement: the [`crud.*` read helpers](/docs/data/crud) strip `.serverOnly()`
285
- columns from every returned row **automatically** — you declare the exposure
286
- policy once at the schema and can't forget it on a handler. For a hand-written
287
- query, omit the column from the `output` schema (and don't put it in the returned
288
- object).
389
+ Enforcement runs in **both directions**, because "never crosses the wire" is not
390
+ a one-way claim:
391
+
392
+ - **Outbound** the [`crud.*` read helpers](/docs/data/crud) strip `.serverOnly()`
393
+ columns from every returned row **automatically**. You declare the exposure
394
+ policy once at the schema and can't forget it on a handler. For a hand-written
395
+ query, omit the column from the `output` schema (and don't put it in the
396
+ returned object).
397
+ - **Inbound** — `crud.create` / `crud.update` **refuse** an input that sets a
398
+ `.serverOnly()` column, with `ServerOnlyColumnWrite` naming it, and write
399
+ nothing. A column the client may not read must not be one the client can set:
400
+ accepting it is mass assignment. It is refused rather than silently stripped
401
+ because a stripped field makes an attack indistinguishable from a no-op. When
402
+ the *server* needs to write one, do it from the handler with
403
+ `ctx.store.insert` / `ctx.store.update` — the refusal is on the generated
404
+ path, which is the one fed straight from client input.
289
405
 
290
406
  A hand-written output is the case `crud.*` cannot cover, so an audit checks it:
291
407
  a wire-reachable query whose `source` table carries a `.serverOnly()` column that
@@ -260,9 +260,27 @@ The `sql` tag is imported from the server-only `@voltro/database/sql` subpath
260
260
  **Raw queries aren't tracked by the reactive engine** — the planner can't infer which tables an arbitrary SQL string touches. If you want a subscription to invalidate on a raw read's tables, declare them explicitly:
261
261
 
262
262
  ```ts
263
- ctx.store.raw!<{ }>(sql`…`, { dependsOn: ['events'] })
263
+ ctx.store.raw!<{ n: number }>(sql`SELECT count(*) AS n FROM events`, { dependsOn: ['events'] })
264
264
  ```
265
265
 
266
+ The tables can also live on the fragment itself (`{ ...frag, dependsOn: ['events'] }`), which is handy when the fragment is built somewhere else. They are taken at face value — the framework records what you declare and never checks it against the SQL.
267
+
268
+ **If you forget, the framework tells you.** A raw read taken while a live query's handler runs, with nothing declared, warns once at subscribe time and names both the query and the SQL:
269
+
270
+ ```
271
+ [voltro] reports.summary: a raw SQL read in this live query declares no
272
+ dependsOn, so no write can invalidate it — every subscriber keeps its first
273
+ result until it reconnects. Declare the tables it reads:
274
+ ctx.store.raw(fragment, { dependsOn: ['orders'] }) — or on the fragment itself.
275
+ The read: SELECT sum(total) FROM orders WHERE tenant = ?
276
+ ```
277
+
278
+ Only the STATIC text is logged; the bound values never are.
279
+
280
+ The diagnostic keeps a bounded record: one request remembers up to 32 distinct raw reads (deduplicated by SQL text), tunable with `VOLTRO_RAW_READ_TRACKING_LIMIT`. Reads past the bound are dropped rather than remembered — a handler that raw-reads in a loop must not turn a warning into a memory leak. Raise it only if a handler issues many distinct raw reads and you want the warning to name a later one.
281
+
282
+ **Where `dependsOn` works, and where it can only warn.** It drives recomputation for a query whose handler returns a **computed value** — the shape whose handler is genuinely re-run on a change (see [Subscriptions](/docs/data/subscriptions)). The declared tables join the query's own `source:`; they never replace it. A handler that returns a query **descriptor** is different: a change re-runs the descriptor's query, not your handler, so the raw result is not refreshed. There the warning is the whole answer — return a computed value if the raw read has to stay live.
283
+
266
284
  ## Tenant scoping (implicit)
267
285
 
268
286
  If the table has the `tenant()` mixin, every `select` auto-merges `WHERE tenantId = ctx.subject.tenantId`. You don't write it; the runtime injects it. To opt out (admin queries crossing tenants), use `.unscoped()`: