@voltro/cli 0.33.0 → 0.35.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 (343) hide show
  1. package/CHANGELOG.md +1968 -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-B8aoJvuw.js +2 -0
  10. package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DYD_ONLD.js} +46 -46
  11. package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
  12. package/dist/appGraph-zuMGKVYX.js +2 -0
  13. package/dist/appPort-B_HpJ_ck.js +48 -0
  14. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  15. package/dist/baselineCommand-DIttzO8A.js +227 -0
  16. package/dist/bin.js +71 -28
  17. package/dist/build-CD8K4XOr.js +711 -0
  18. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  19. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  20. package/dist/checkCommand-Ct9xkTrS.js +232 -0
  21. package/dist/checkCommand-DKpDLlqu.js +2 -0
  22. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  23. package/dist/cliError-BmdYnghb.js +10 -0
  24. package/dist/cliOutput-D1tSBoRM.js +15 -0
  25. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  26. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  27. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  28. package/dist/clusterCmd-D5wsCmA_.js +54 -0
  29. package/dist/codegen-CYM3Zqrf.js +605 -0
  30. package/dist/codegen-ChBi_hVa.js +2 -0
  31. package/dist/codegenCommand-C4YoQIc2.js +30 -0
  32. package/dist/codemodRunner-BnFq3Fgu.js +5384 -0
  33. package/dist/commandRunner-BLAEFLjp.js +47 -0
  34. package/dist/commands-BE8E7zF3.js +816 -0
  35. package/dist/connectionConfig-UFlIEiys.js +66 -0
  36. package/dist/dashboardCommand-D7SgZGaN.js +25 -0
  37. package/dist/dataCommand-BhYwDgg-.js +537 -0
  38. package/dist/dataProfile-dW-PsfLB.js +15 -0
  39. package/dist/dbCommand-DS4b97Is.js +2 -0
  40. package/dist/{dbCommand-DTLKAfbA.js → dbCommand-O8HA63s2.js} +552 -402
  41. package/dist/{dev-C_P8FLSx.js → dev-C7sFZq3m.js} +3670 -3169
  42. package/dist/dev-D2BikO7a.js +3 -0
  43. package/dist/devActivity-Dx_3nnGv.js +100 -0
  44. package/dist/devActivity.js +1 -1
  45. package/dist/dialectDriver-CgXnDfec.js +39 -0
  46. package/dist/discover-C9XKJDco.js +25 -0
  47. package/dist/doctorCommand-CM4Ch9C7.js +2 -0
  48. package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-DnimF5IM.js} +523 -1278
  49. package/dist/dormancyCommand-QewYug_s.js +69 -0
  50. package/dist/e2eCmd-BRabZww-.js +147 -0
  51. package/dist/embeddingsCommand-BfiLS_QI.js +73 -0
  52. package/dist/envCommand-CCGPRQY1.js +60 -0
  53. package/dist/evalCommand-6RUfPen4.js +118 -0
  54. package/dist/evolveCommand-CHsLCtDf.js +281 -0
  55. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  56. package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
  57. package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
  58. package/dist/generateCommand-DbgcUpGw.js +147 -0
  59. package/dist/index.d.ts +45 -0
  60. package/dist/index.js +4 -3
  61. package/dist/infoCommand-DwOgK1t6.js +60 -0
  62. package/dist/{inspect-BUUjt773.js → inspect-CjTYzAs_.js} +113 -41
  63. package/dist/inspect-P4pxoMaV.js +2 -0
  64. package/dist/inspectCmd-EHFZ9yYu.js +224 -0
  65. package/dist/inspectFetch-EMuhTG_9.js +151 -0
  66. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  67. package/dist/loadEnv-D9nEOClM.js +44 -0
  68. package/dist/logFileSink-C_D2wRN1.js +105 -0
  69. package/dist/logsCmd-D36xK7Zu.js +260 -0
  70. package/dist/manifestBuild-COkJoyAr.js +2 -0
  71. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-hpPLaGxV.js} +1 -1
  72. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  73. package/dist/migrate-BV7I-ZHZ.js +83 -0
  74. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  75. package/dist/newCommand-COWOJ1_E.js +156 -0
  76. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  77. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  78. package/dist/pageConvention-cEiRxdab.js +5 -0
  79. package/dist/privacyCommand-C-Df56U_.js +146 -0
  80. package/dist/probeCommand-CZfaaUOZ.js +122 -0
  81. package/dist/projectScaffold-DmzEKHib.js +2 -0
  82. package/dist/projectScaffold-LMMtaavR.js +814 -0
  83. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  84. package/dist/renderProfile-1OWWAAtx.js +81 -0
  85. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  86. package/dist/runtimeTrace-CH3eUiMw.js +91 -0
  87. package/dist/scheduleCmd-DQRu6BZC.js +149 -0
  88. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  89. package/dist/schemaIr-UJybUUZW.js +103 -0
  90. package/dist/{sdkgen-C81QIkiL.js → sdkgen-BLkvGRfX.js} +111 -209
  91. package/dist/seedRunner-ZmLSqNe2.js +333 -0
  92. package/dist/serveCommand-CbDHU6l-.js +2 -0
  93. package/dist/serveCommand-iwlUBNS1.js +1766 -0
  94. package/dist/serveEntry.js +5 -5
  95. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  96. package/dist/start-BgN62boB.js +3 -0
  97. package/dist/start-T4VesWiM.js +1087 -0
  98. package/dist/startEntry.js +2 -2
  99. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  100. package/dist/storageCommand-Co6NfLqN.js +42 -0
  101. package/dist/templates-De8IR5-c.js +102 -0
  102. package/dist/test-CI6iDsYc.js +115 -0
  103. package/dist/tracesCmd-DStmCJPi.js +232 -0
  104. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  105. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  106. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  107. package/dist/updateCommand-BlMZhWgO.js +2 -0
  108. package/dist/updateCommand-x0pI_x-B.js +585 -0
  109. package/dist/webDev-BcykISYQ2.js +2 -0
  110. package/dist/{inspectMetrics-1xzTKAFx.js → webDev-Dybxew86.js} +988 -1560
  111. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  112. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  113. package/dist/webhooksCommand-DlAgS2Iw.js +267 -0
  114. package/dist/workflowsCmd-BGF-mRZ5.js +608 -0
  115. package/package.json +209 -17
  116. package/templates/AGENTS.core.md +58 -3
  117. package/templates/AGENTS.md +64 -7
  118. package/templates/agent-docs/_index.md +6 -4
  119. package/templates/agent-docs/_manifest.json +23 -6
  120. package/templates/agent-docs/ai.md +370 -0
  121. package/templates/agent-docs/authentication.md +313 -31
  122. package/templates/agent-docs/caching.md +6 -0
  123. package/templates/agent-docs/cli.md +853 -50
  124. package/templates/agent-docs/data.md +608 -12
  125. package/templates/agent-docs/database/migrations.md +223 -25
  126. package/templates/agent-docs/database/misc.md +156 -40
  127. package/templates/agent-docs/database/querying.md +19 -1
  128. package/templates/agent-docs/database/scaling.md +60 -0
  129. package/templates/agent-docs/database/schema.md +1 -0
  130. package/templates/agent-docs/database/seedsdialects.md +208 -19
  131. package/templates/agent-docs/database/transactions.md +68 -0
  132. package/templates/agent-docs/deployment.md +284 -25
  133. package/templates/agent-docs/introduction.md +88 -17
  134. package/templates/agent-docs/local-first-mobile.md +79 -4
  135. package/templates/agent-docs/multi-tenancy.md +188 -42
  136. package/templates/agent-docs/observability.md +58 -3
  137. package/templates/agent-docs/plugins/ai-flows.md +247 -2
  138. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  139. package/templates/agent-docs/plugins/audit.md +37 -1
  140. package/templates/agent-docs/plugins/auth-social.md +143 -0
  141. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  142. package/templates/agent-docs/plugins/auth.md +131 -6
  143. package/templates/agent-docs/plugins/billing.md +132 -15
  144. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  145. package/templates/agent-docs/plugins/clickhouse.md +32 -1
  146. package/templates/agent-docs/plugins/duckdb.md +1 -1
  147. package/templates/agent-docs/plugins/flags.md +132 -0
  148. package/templates/agent-docs/plugins/governance.md +105 -7
  149. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  150. package/templates/agent-docs/plugins/presence.md +13 -2
  151. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  152. package/templates/agent-docs/plugins/search.md +162 -8
  153. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  154. package/templates/agent-docs/plugins/storage.md +11 -0
  155. package/templates/agent-docs/plugins/webhooks.md +105 -0
  156. package/templates/agent-docs/plugins.md +152 -18
  157. package/templates/agent-docs/reference.md +60 -3
  158. package/templates/agent-docs/releases.md +1117 -0
  159. package/templates/agent-docs/routing.md +43 -25
  160. package/templates/agent-docs/scheduling.md +27 -0
  161. package/templates/agent-docs/schema-driven-ui.md +92 -12
  162. package/templates/agent-docs/security.md +426 -0
  163. package/templates/agent-docs/templates/apibackends.md +87 -18
  164. package/templates/agent-docs/templates/appshells.md +32 -14
  165. package/templates/agent-docs/templates/overview.md +13 -8
  166. package/templates/agent-docs/testing.md +211 -14
  167. package/templates/agent-docs/whats-new.md +98 -136
  168. package/templates/agent-docs/workflows.md +231 -14
  169. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  170. package/templates/apps/api-ai/package.json +8 -7
  171. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  172. package/templates/apps/api-auth/package.json +9 -8
  173. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  174. package/templates/apps/api-backend/package.json +12 -8
  175. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  176. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  177. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  178. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  179. package/templates/apps/api-backend-deactivation/package.json +8 -7
  180. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  181. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  182. package/templates/apps/api-backend-mail/package.json +9 -8
  183. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  184. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  185. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  186. package/templates/apps/api-backend-mariadb/package.json +10 -9
  187. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  188. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  189. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  190. package/templates/apps/api-backend-sqlite/package.json +9 -8
  191. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  192. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  193. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  194. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  195. package/templates/apps/api-backend-storage/package.json +9 -8
  196. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  197. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  198. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  199. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  200. package/templates/apps/api-cms/app.config.ts +19 -0
  201. package/templates/apps/api-cms/authz.ts +63 -0
  202. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  203. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  204. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  205. package/templates/apps/api-cms/package.json +11 -10
  206. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  207. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  208. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  209. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  210. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  211. package/templates/apps/api-collab/package.json +9 -8
  212. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  213. package/templates/apps/api-data-advanced/package.json +9 -8
  214. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  215. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  216. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  217. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  218. package/templates/apps/api-durable/package.json +9 -8
  219. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  220. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  221. package/templates/apps/api-feature-flags/package.json +10 -9
  222. package/templates/apps/api-governance/README.md +30 -8
  223. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  224. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  225. package/templates/apps/api-governance/database/schema.ts +16 -4
  226. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  227. package/templates/apps/api-governance/package.json +9 -8
  228. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  229. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  230. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  231. package/templates/apps/api-kv/package.json +9 -8
  232. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  233. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  234. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  235. package/templates/apps/api-moderation/package.json +9 -8
  236. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  237. package/templates/apps/api-observability/package.json +9 -8
  238. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  239. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  240. package/templates/apps/api-ratelimit/package.json +9 -8
  241. package/templates/apps/api-rbac/package.json +9 -8
  242. package/templates/apps/api-rest/package.json +8 -7
  243. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  244. package/templates/apps/api-saas/package.json +12 -11
  245. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  246. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  247. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  248. package/templates/apps/api-saas-starter/authz.ts +75 -0
  249. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  250. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  251. package/templates/apps/api-saas-starter/package.json +15 -11
  252. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  253. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  254. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  255. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  256. package/templates/apps/api-search/package.json +9 -8
  257. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  258. package/templates/apps/api-status/README.md +10 -3
  259. package/templates/apps/api-status/app.config.ts +8 -3
  260. package/templates/apps/api-status/authz.ts +5 -3
  261. package/templates/apps/api-status/package.json +9 -8
  262. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  263. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  264. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  265. package/templates/apps/api-status/tests/status.test.ts +9 -1
  266. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  267. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  268. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  269. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  270. package/templates/apps/api-versioning/package.json +9 -8
  271. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  272. package/templates/apps/api-webhooks/package.json +10 -9
  273. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  274. package/templates/apps/changelog/package.json +8 -6
  275. package/templates/apps/edge-functions/package.json +2 -2
  276. package/templates/apps/frontend-admin/package.json +10 -8
  277. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  278. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  279. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  280. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  281. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  282. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  283. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  284. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  285. package/templates/apps/frontend-app/app.config.ts +4 -3
  286. package/templates/apps/frontend-app/package.json +11 -8
  287. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  288. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  289. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  290. package/templates/apps/frontend-auth/package.json +10 -8
  291. package/templates/apps/frontend-blank/package.json +9 -7
  292. package/templates/apps/frontend-cms/package.json +11 -9
  293. package/templates/apps/frontend-collab/package.json +12 -9
  294. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  295. package/templates/apps/frontend-contact/package.json +9 -7
  296. package/templates/apps/frontend-dashboard/package.json +9 -7
  297. package/templates/apps/frontend-docs/package.json +9 -7
  298. package/templates/apps/frontend-i18n/package.json +8 -6
  299. package/templates/apps/frontend-landing/package.json +9 -7
  300. package/templates/apps/frontend-portal/package.json +10 -8
  301. package/templates/apps/frontend-saas/app.config.ts +10 -6
  302. package/templates/apps/frontend-saas/package.json +10 -8
  303. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  304. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  305. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  306. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  307. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  308. package/templates/apps/frontend-spa/package.json +9 -7
  309. package/templates/apps/frontend-ssr/package.json +9 -7
  310. package/templates/apps/frontend-ssr-api/package.json +10 -8
  311. package/templates/apps/frontend-static-blog/package.json +8 -6
  312. package/templates/apps/frontend-status/package.json +10 -8
  313. package/templates/apps/mobile-app/README.md +1 -0
  314. package/templates/apps/mobile-app/package.json +4 -2
  315. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  316. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  317. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  318. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  319. package/templates/baselines/bare/.env.example +14 -0
  320. package/templates/baselines/bare/baseline.json +4 -4
  321. package/templates/baselines/compose/.env.example +14 -0
  322. package/templates/baselines/compose/README.md +1 -1
  323. package/templates/baselines/compose/baseline.json +5 -5
  324. package/templates/baselines/compose-mariadb/.env.example +14 -0
  325. package/templates/baselines/compose-mariadb/README.md +1 -1
  326. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  327. package/templates/baselines/helm/.env.example +14 -0
  328. package/templates/baselines/helm/baseline.json +4 -4
  329. package/dist/apiBuild-C-x9YacA.js +0 -2
  330. package/dist/appGraph-CvQCte0z.js +0 -2
  331. package/dist/checkCommand-DRovTKza.js +0 -2
  332. package/dist/commands-CJfepbm4.js +0 -11541
  333. package/dist/dbCommand-b1gum4td.js +0 -2
  334. package/dist/dev-iiMtlkfs.js +0 -3
  335. package/dist/devActivity-BhIu6ncs.js +0 -159
  336. package/dist/frameworkTableAssembly-BwIrO5nv.js +0 -638
  337. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  338. package/dist/inspect-mmBuRXmy.js +0 -2
  339. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  340. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  341. package/dist/serveCommand-DdaM4Hup.js +0 -1608
  342. package/dist/start-C0koT0UO.js +0 -1084
  343. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -1,4 +1,4 @@
1
- # What's new in 0.33.0
1
+ # What's new in 0.35.0
2
2
 
3
3
  Read this FIRST when a task touches an area you have not worked in recently.
4
4
  It is the cheapest way to notice that the framework grew the thing you were
@@ -9,201 +9,163 @@ BREAKING entries name a codemod; run `voltro update` to apply it.
9
9
 
10
10
  ### ⚠ BREAKING
11
11
 
12
- - **@voltro/protocol, @voltro/runtime, @voltro/voltro** — `CoordinatedScheduleHandle` gained `wake()`, `currentIntervalMs()` and `isArmed()`.
12
+ - **@voltro/runtime, @voltro/cli, @voltro/plugin-clickhouse, @voltro/plugin-duckdb, @voltro/plugin-analytics-postgres** — The analytics CDC-mirror's version is derived from the CHANGE under `changeScope: 'fleet'` (postgres CDC, mysql binlog) — warehouse baseline plus the change's per-key position in the totally-ordered fleet stream — instead of each replica's own clock. An N-replica deployment still issues N duplicate writes per change (every replica observes the whole stream; that is the transport), but they are now BYTE-IDENTICAL — same row image, same version — so the sinks' existing guards (ClickHouse `ReplacingMergeTree(version)`, DuckDB/postgres `excluded.version > version`) dedupe them for free, with no leader election and no clock anywhere. This closes the real defect behind the N× cost: under clock skew larger than the gap between two changes to one row, a peer's duplicate of the OLDER image could take the higher version and win in the warehouse permanently and silently. A replica joining mid-stream seeds each key's numbering from the warehouse's own high-water mark via the new REQUIRED `AnalyticsMirrorImpl.maxVersion` read (all shipped warehouse sinks implement it; tombstoned deletes keep their version so the read answers after a delete — a custom sink follows the codemod note). The once-per-boot `changeScope=fleet` warning that named this cost is REMOVED — the hazard it named is gone. Local-scope stores keep the hybrid-clock version unchanged. New tunable: `VOLTRO_ANALYTICS_MIRROR_VERSION_STATE_LIMIT` bounds the per-key version state (default 100000; least-recently-changed keys re-seed from the warehouse on their next change).
13
+ - **@voltro/cli** — The declared framework table set no longer reads a runtime flag. `CDC`, `VOLTRO_UNDO` and `VOLTRO_TRACING_PERSIST` each moved it before this release; `app.config.ts` gained `schema: { traces?, undo? }` to declare the two that still need a decision.
13
14
 
14
- The type change that carries the poller work in this release (see *A coordinated tick is a FLOOR*). Two of the three shapes it touches are NOT breaking and are listed here so the classification is checkable rather than asserted:
15
+ A consumer measured two fingerprints from one source tree, one database and one `NODE_ENV`, differing only in `CDC`. Their Helm chart gives the pre-upgrade migrate Job its own `env:` list — `NODE_ENV`, `DB_*`, the obvious migration inputs — while `CDC: "0"` lives in the pods' block, because change data capture is obviously a runtime concern. Nothing about the name reads as schema-affecting, so it was in none of their three overlays' jobs. The declared set is what the schema fingerprint hashes, so that is a GREEN migrate job followed by every pod refusing to boot. Latent for months, and it would have fired on their next deploy.
15
16
 
16
- - the effect parameter was **widened** it may now return a tick outcome, and an existing `() => Promise<void>` still satisfies it; - `Coordinator.tryClaim` gained an **optional** third parameter (the caller's bucket width), so an existing implementation still conforms.
17
+ Counting the family after their report found three, not one measured on a mariadb app at `NODE_ENV=production`, each flag flipped alone: `CDC=0` removed `_voltro_cdc_offsets`, `VOLTRO_UNDO=on` added `_voltro_undo_log`, `VOLTRO_TRACING_PERSIST=all` added `_voltro_traces`. All three are the kind of value an operator puts on the pods and not on the job, and only one of them had been noticed.
17
18
 
18
- (The plugin-facing `scheduleCoordinated` also gained an OPTIONAL fourth argument, `{ disarmWhenIdle }` additive, and how a plugin opts its own task out of polling entirely.)
19
+ `NODE_ENV` had already produced this exact failure in 0.34.0 and was fixed by making one decider resolve it for every command. That fix does not generalise here: a job legitimately does not carry an observability flag, so there is nothing to agree on. The rule is therefore stated rather than patched — **the declared set may depend only on inputs every process in one deployment computes identically** (the source tree, `app.config.ts`, the dialect, and `NODE_ENV`), and `declaredSchemaGates.test.ts` sweeps every `VOLTRO_*` / `CDC` / `DB_*` name the framework reads anywhere in `packages/*/src` and fails if any of them moves the set. Derived rather than listed, because a test naming the three known offenders only re-checks what someone already remembered — which is how the two unreported ones survived.
19
20
 
20
- What breaks is code that **constructs** a handle rather than receiving one: a hand-written test double of `PluginBindContext`, which is the ordinary way to unit-test a plugin's `bindDataStore`. Four of the framework's own suites carried one, and three of those compiled only because the stub was cast which is also why the two new members must be REQUIRED rather than optional. An optional `wake()` would let a caller subscribe a change channel to a handle that silently has none, and a poller that never wakes is the failure this release exists to remove, arriving quietly.
21
+ The three got two different answers, deliberately. `CDC` left the derivation entirely: `_voltro_cdc_offsets` follows the DIALECT now, so a mariadb or mssql app declares it whether or not that process drives CDC. The cost is one empty offsets table and it is the same trade `impliesScheduleTables` already makes in writing. `VOLTRO_UNDO` and `VOLTRO_TRACING_PERSIST` could not simply be dropped both can legitimately turn a table on in production, and a declared set that ignored them would leave capture writing to a table nobody created so they keep their runtime meaning and lose their declaring power. Turning capture OFF still needs no declaration and never will; turning it ON without one is refused at boot, on both boot paths, with the config field named.
21
22
 
22
- The codemod is `manual`: the object literal needing the two fields carries no importable symbol and usually sits behind an `as never`, so no transform can tell it apart from an unrelated literal in the same test file. It is gated on the app mentioning `scheduleCoordinated` at all.
23
- - **@voltro/runtime** — `@effect/opentelemetry` is now an **optional peer** of `@voltro/runtime` instead of a dependency. **If you export traces or metrics, install it:**
23
+ The `prod-mismatch` refusal also stopped being two hashes and a command. It prints which of the three decided tables THIS process declared and from which input, because the ledger stores no table set to diff against and the command it used to recommend was the one the operator had just run successfully. That half matters beyond the framework's own tables: a plugin's `extendSchema.tables` is app code and can read anything, so the rule above cannot be enforced for it.
24
24
 
25
- ```sh
26
- pnpm add @effect/opentelemetry
27
- ```
28
-
29
- If you do not (no `FRAMEWORK_TRACING`, no `FRAMEWORK_METRICS`, no `OTEL_EXPORTER_OTLP_*`), nothing changes and your install gets 24 lines quieter.
30
-
31
- It is reached from one dynamic `import()`, only when tracing is on, and it declares seven non-optional OpenTelemetry peers of which we supply five. So every `pnpm install` of every consumer ended with an unmet-peer block describing a condition that broke nothing. Declaring the two missing peers as real dependencies was the wrong direction — one of them is `@opentelemetry/sdk-trace-web`, the BROWSER tracer — and 0.64.0 is the current stable, so there is no upstream release marking them optional to wait for.
32
-
33
- The reporting consumer's argument is what decided it: *"a check that is loud on every upgrade teaches people to skip the output, and the next warning in that block is the one that matters. We read past this one for four releases."*
34
-
35
- A boot with tracing enabled and the package absent fails with a message naming this install line — a startup failure, not a silent loss of telemetry.
25
+ **`voltro update` carries you across this** — codemod `0.35.0/05_declared-schema-drops-runtime-flags`.
26
+ - **@voltro/protocol, @voltro/runtime, @voltro/cli, @voltro/devtools-ui** — A declared event must decide who may listen — the boot gate now covers `defineEvent`, closing SEC-1's sibling. `defineEvent`'s `guards:` was optional and `bindEvent` skipped an empty list, so under `security.defaultDeny` an event with NO access declaration was silently subscribable by anyone who could open the socket, while the identical shape was already refused for every procedure.
36
27
 
37
- **`voltro update` carries you across this**codemod `0.33.0/01_opentelemetry-optional-peer`.
28
+ `defineEvent` now accepts `openAccess: '<reason>'` mutually exclusive with `guards:`, reason string required exactly as the four procedure definers do. The erased `{ open }` decision rides the same `guards` array every enforcement path reads; `bindEvent` treats it as "no check" (an open event pays what an unguarded one pays: nothing), and `eventToRpc` no longer unions `ScopeError` into the wire contract for an event that cannot produce a denial.
38
29
 
39
- ### Added
30
+ **Breaking for `security.defaultDeny` apps (the default):** an app with a `*.event.ts` declaring neither `guards:` nor `openAccess:` now refuses to boot under `voltro dev` and `voltro serve`, naming every undecided event — the same message, from the same gate, procedures get. `voltro doctor` lists the same set. Migration: give each event a decision (`guards: [{ scope: '…' }]` or `openAccess: '<why anyone may listen>'`); an app that wants the old default-allow declares `security: { defaultDeny: false }` once, in `app.config.ts`. Plugin-declared events are not judged — the gate reads the app's own discovered files only.
40
31
 
41
- - **@voltro/cli** `voltro agents-md` now reports which `@voltro/cli` it seeded from, and warns when that is not the one the project installs.
32
+ The events inspect snapshot (and the devtools Events panel) now counts only ENFORCEABLE guards and carries the `openAccess` reason, so a deliberately open event renders as "open access" instead of as "1 guard" over an event anyone may subscribe to.
42
33
 
43
- ```
44
- seeded from @voltro/cli 0.31.0 (project has 0.32.0; whats-new describes 0.31.0;
45
- modules COPIED into ./agent-docs)
46
- WARN the `voltro` binary that ran is 0.31.0, but this project installs 0.32.0 —
47
- everything just written describes the OLDER version.
48
- ```
34
+ **`voltro update` carries you across this** — codemod `0.35.0/01_event-access-decision`.
35
+ - **@voltro/plugin-ai-flows** **The breaking half, first:** `RunStepStatus` gained `'skipped'`. A sixth member means an exhaustive switch over it stops compiling and a status-keyed lookup has a hole — so a manual codemod fires on any app that names the type or its literals. Everything else here is additive (optional fields, new exports, one nullable column that rides the declarative differ).
49
36
 
50
- A consumer reported a freshly-seeded `agent-docs/whats-new.md` one release behind their installed version, twice. The published packages are correct (verified with `npm pack`), so the content came from a different `@voltro/cli` than the one they installed — the command reads its templates relative to the RUNNING binary, and a globally-installed `voltro`, a stale `dist`, or a parent workspace's copy all produce exactly that, with output that looked identical either way.
37
+ Flows can now BRANCH, FAN OUT, and no longer chain without a bound. Three additions, and the third is a defect fix wearing a feature's clothes.
51
38
 
52
- It does not refuse and does not pick a cli for you: running the workspace binary against a checkout is legitimate and common.
53
- - **@voltro/cli** — `voltro db encrypt-column <table>.<column>` — the data migration `.encrypted()` always needed.
39
+ **`when:` a conditional step.** A step runs only if its condition holds against the run context; a false condition SKIPS the step rather than failing it, so it produces no output and anything referencing it sees an absent value. The run timeline carries the rendered reason (`{{mode}} equals "full"`), because a step that silently vanished is indistinguishable from a step nobody declared.
54
40
 
55
- `.encrypted()` encrypts on WRITE, so adding it to a populated column converts nothing that is already there, and there was no supported way to convert it. A consumer carried three plaintext credential columns for months with no next step: *"`.encrypted()` braucht einen Cipher UND eine Datenmigration der bestehenden Zeilen; gemeldet, nicht behoben."*
56
-
57
- ```sh
58
- voltro db encrypt-column integrations.webhookSecret --dry-run
59
- voltro db encrypt-column integrations.webhookSecret employees.meilisearchKey --yes
60
- ```
41
+ The condition is STRUCTURED data (`{ ref, op, value }`), not an expression string, and that is three decisions in one. A flow can be authored as a stored row a user edits in a browser — an expression there is an evaluator running user-authored source on the server. The visual editor can offer a dropdown over a structure and cannot over a string it would have to parse. And `validateFlow` already walks every reference, so a structured `ref` joins that check for free: a typo'd condition would otherwise evaluate absent, take the false branch, and skip its step on every run, forever, with nothing logged.
61
42
 
62
- Five guards, each for a way a naive version succeeds and destroys data:
63
-
64
- - **Idempotent** — an already-ciphertext value is skipped, so an interrupted run is resumed by running it again. Double encryption is unrecoverable without the key history. - **Round-trip verified before the write** — every value is decrypted back in-process first, so a broken cipher fails with nothing written. - **Key checked against what the column already holds** — a *different* key round-trips fine, so the check above cannot see it. Resuming with the wrong key would leave a column readable with neither key alone. - **Width pre-flight** — ciphertext is `49 + 4×ceil(bytes/3)` characters, so a 64-char key needs 137 and a `varchar(100)` fails partway. Refuses with both numbers and the `.maxLength()` to set. Measured in BYTES: `'ä'.repeat(10)` is 10 characters and 20 bytes. - **`--yes` required**, `--dry-run` shows the counts, and no value — plaintext or ciphertext — is ever printed.
65
-
66
- Verified against a real postgres: the conversion, the re-run no-op, both refusals writing nothing, and a decrypt back to the original including multi-byte content.
67
-
68
- ### Changed
69
-
70
- - **@voltro/runtime** — `_voltro_schedule_claims` swaps its `(scheduleName, bucket)` index for `(scheduleName, claimedAt)`.
71
-
72
- A consumer read `pg_stat_user_indexes` on their live table and measured, over its whole lifetime:
73
-
74
- ```
75
- _voltro_schedule_claims_pkey 348 978 scans
76
- _voltro_schedule_claims_claimedAt_idx 3 949
77
- _voltro_schedule_claims_scheduleName_bucket_idx 4
78
- ```
43
+ Truthiness here deliberately differs from JavaScript's: `0` and `''` are TRUTHY. A step gated on a generated count or string means "did the producer run", not "is it non-zero" — the second is `{ op: 'neq', value: 0 }`, sayable when meant.
79
44
 
80
- Four. It was declared "for the case where you would rather ask by field", and nothing ever asks by field every read of this table goes through the primary key, which *is* `<scheduleName>@<bucket>`. An index nothing uses is not free: it is written on every INSERT, into a table written once per tick per schedule.
45
+ **`group:` concurrent steps.** Consecutive steps sharing a group name run at the same time, each keeping its own durable step, so a replay resolves every branch from the journal exactly as it would sequentially. Kept as a flat field rather than a nested `parallel([...])` because the durable step name, the run timeline and a `human` step's signal name are all INDEX-keyed — nesting would re-index every flow already running.
81
46
 
82
- `(scheduleName, claimedAt)` is the shape of a query that now exists the per-schedule prune a winning claim runs (`WHERE scheduleName = ? AND claimedAt < ?`). The `claimedAt` index stays: the retention sweep's cutoff spans every schedule and needs it leading, which the composite cannot provide.
47
+ Three rules, all enforced at registration: grouped steps cannot read each other's outputs (they have no order between them), a group must be contiguous (a name that stops and resumes would run as two sequential fan-outs), and a `human` review cannot join a group (it suspends the whole run). Context writes are applied after the whole segment in AUTHORED order applying them as branches land would make the run context depend on scheduling, which a durable replay must never do.
83
48
 
84
- No codemod: a `_voltro_*` change rides the declarative differ on `voltro db apply` and on a `voltro dev` boot, on every dialect.
49
+ **A chain is bounded — this half is a fix.** `chainTo` carried exactly one guard, a flow could not chain to itself, so `A B → A` and any deep chain were unbounded: each hop starts a child run with a fresh idempotency key, so nothing collapsed it and nothing was counting the hops. A run now carries the chain that led to it ON THE PAYLOAD — deliberately not reconstructed from the run rows, because a guard whose evidence comes from a query is a guard that permits the loop whenever the query fails. A chain is refused on a cycle, or at `maxChainDepth` (default 5; `aiFlowsPlugin({ maxChainDepth })` or `VOLTRO_AI_FLOW_MAX_CHAIN_DEPTH`), and the refusal lands on the run row's new `chainRefusal` column naming the path. The parent run still SUCCEEDS: a refused follow-up is a configuration problem, not a reason to destroy a completed result.
85
50
 
86
- The same measurement corrected something the reporter had said in an earlier round and we had repeated back to them — that both indexes went unused. The primary key is used constantly. That makes the finding sharper rather than weaker: the ability to answer this question in one lookup is not merely available, it is demonstrably in use on the same table, and the one read path that needed it was the one not taking it.
51
+ All three are driven through the REAL durable executor in tests, not just their pure helpers a primitive that is correct and reaches nothing is the defect class this package's own segmentation module exists to prevent.
52
+ - **@voltro/protocol, @voltro/runtime, @voltro/cli, @voltro/ai, @voltro/plugin-billing, @voltro/plugin-flags, @voltro/plugin-governance, @voltro/plugin-notifications, @voltro/plugin-presence, @voltro/plugin-storage** — Every first-party plugin rpc route now declares an access decision (`guards:` or `openAccess: '<reason>'`), and `security.defaultDeny` is enforced in the DISPATCH spine as defense in depth behind the boot gate: a descriptor that reaches the wire with no decision (a third-party plugin route, an embedder's hand-bound descriptor) is refused per-request with a typed `ScopeError` before the transaction / external I/O. Twelve previously-open routes now require a scope: `billing.startCheckout` / `portalUrl` / `previewChange` / `changePlan` / `changeSeats` / `invoices` → `billing:manage`; `billing.reportUsage` → `billing:report`; `governance.export` / `erase` → `admin:full` (already enforced in-handler, now declared); `storage.mintUploadUrl` / `ingestUrl` → `storage:manage`; `storage.listRefs` → `storage:browse`. Migration: grant the scope to the role/subjects that legitimately hold each capability (rbac role, `resolveScopes`, api-key scopes) — the codemod lists every route and the open-by-design surfaces that did NOT change. `PluginRpcRoute` gains `guards`/`openAccess` fields, carried through the route lift into the enforced descriptor; the synthesized agent/undo/connections built-ins declare `openAccess` so they keep serving under default-deny.
87
53
 
88
- ### Fixed
89
-
90
- - **@voltro/database** — A column ADDED with a `reference()` now gets its foreign key in the same plan.
91
-
92
- `ADD COLUMN` emits no `REFERENCES` clause on any dialect, and the planner's FK branch lived only in the path for a column present on both sides — so adding a `reference()` column to an existing table planned an `add-column` and nothing else. The constraint appeared on the SECOND `voltro db apply`, when the column was live and the diff finally saw a live column with no FK.
93
-
94
- Two applies converged, so the state was reachable, which is why this survived as a low-priority note for a long time. It is worse under `voltro dev`: the boot diff refuses to record a fingerprint while the re-plan is non-empty, so an app whose only pending change was such a column re-planned on every boot and never converged.
95
-
96
- Both callers share one `addForeignKeyOps` builder now, and the existing dependency tiering already orders `add-column` before `add-foreign-key`.
97
- - **@voltro/runtime, @voltro/protocol, @voltro/workflow, @voltro/cli** — A coordinated tick is a FLOOR now, and a claim no longer outlives its bucket.
98
-
99
- A consumer's `_voltro_schedule_claims` reached **86 214 rows / 33 MB** on two days of uptime and took their deployment down: ten of a fifteen-slot pooler pinned on the claim read, an SSR render measured at **300 490 ms** behind them, every page in three frontends unusable, and a `rollout restart` that could not complete because the surge pod could not get a connection. Two hours of their own measurement produced the diagnosis, and both halves of it were right.
100
-
101
- **Where the rows came from.** They declare one workflow, have never started it, use no flow control and no offloaded inference. Over one hour, with two replicas:
54
+ ### Added
102
55
 
103
- ```
104
- voltro.ai.inference 1 259 rows/h (250 ms ticks) framework
105
- voltro.workflow.admission 1 247 rows/h (1 s ticks) framework
106
- their own eight schedules 18 rows/h
107
- ```
56
+ - **@voltro/workflow, @voltro/cli** — `awaitSignal` now logs a one-time hint (once per workflow, never per poll) when its declared `timeoutMs` exceeds a threshold, naming `awaitSignalSuspending` — the drop-in variant that SUSPENDS the run and frees the worker slot for human-approval-length waits (WF-11). Threshold: `workflows: { suspendSignalHintMs }` in `app.config.ts` (default 5 minutes), env override `VOLTRO_WORKFLOW_SUSPEND_HINT_MS`. A hint only — the framework never swaps the variant under a run, because the two journal differently and a silent swap mid-history is a replay trap.
57
+ - **@voltro/data-transfer, @voltro/sql-postgres, @voltro/cli** — The logical importer bulk-loads postgres targets via `COPY … FROM STDIN` (PERF-13). `voltro data import` engages it automatically wherever plain-INSERT semantics provably hold — `--mode replace`, or the default `upsert` into a table that is empty at import time (the fresh-target shape of every cross-dialect migration) — and never under `--atomic`. A refused COPY batch is atomic (nothing landed), so the importer replays exactly that batch through the per-row path with held-row / deferred-FK semantics intact. MEASURED on a 7-column table (text/int/bool/jsonb/timestamptz), 50 000 rows, local postgres: row-by-row 12.8 s (~3.9 k rows/s) vs COPY 0.59 s (~84.6 k rows/s) — **21.7×**. New seams: `ImportOptions.copyLoader` / `copyBatchSize` (default 5000) in `@voltro/data-transfer`, and `makePgCopySession` / `encodeCopyRow` in `@voltro/sql-postgres` (a submittable CopyIn query over the existing `pg` driver — no new dependency). Other dialects keep the per-row writes.
58
+ - **@voltro/cli** `voltro probe access` asks a RUNNING app whether its declared access is actually enforced — the question none of the existing checks ask.
108
59
 
109
- 99.3 % of the ledger was the framework polling two structurally empty queues. A fixed interval has no way to learn that, so:
60
+ `voltro check`, the boot access gate and `security.defaultDeny` all verify that a decision was DECLARED. None of them verifies that the declaration REFUSES anyone. That distinction is not hypothetical here: the dispatch spine and the boot gate were separate for several releases, a procedure filtered out of the rpc group while still bound in the handler map served silently on one path and crashed the other, and `check` itself counted a decided-open route as guarded. Every one was the declaration and the behaviour disagreeing, found by reading rather than by asking.
110
61
 
111
- - **`scheduleCoordinated`'s effect may now REPORT its tick.** Return `{ idle: true }` and the runner backs off toward a ceiling; return `{ idle: true, nextDueInMs }` and it arms for that instant instead — which is what keeps a `debounce` window from being slept through. Returning nothing keeps the fixed interval, so every existing plugin task ticks exactly as before. - **Where an arrival is guaranteed to wake it, an idle task STOPS ENTIRELY** (`{ disarmWhenIdle: true }`). Both framework tasks do, on any deployment where a peer replica's write is visible locally Postgres LISTEN/NOTIFY, or a broadcast broker. Measured against a real Postgres on a deployment that uses neither queue: **2 claim rows in five minutes**, one per task, both at boot. Where that guarantee does not hold, the ceiling (`VOLTRO_POLL_CEILING_MS`, default 30 s) is the correct behaviour and is what they get. - **`handle.wake()` runs a tick now.** Both framework queues are tables with the framework's own CDC triggers on them, so an enqueue already produces a change event on every replica; both dispatchers subscribe to it. The idle case gets ~120× cheaper and the busy case gets FASTER — work starts on the INSERT rather than up to a tick later. - The claim bucket stays floored by the BASE interval. Replicas do not share a backoff state, and two replicas computing different keys for one moment would both win.
62
+ It calls every guarded procedure with NO credentials and reports three verdicts: `refused` (enforcement works), `admitted` (the finding), and `inconclusive` — the call failed for a reason that is not an access refusal, usually payload validation running before the guard. `inconclusive` is never counted as a pass; `--strict` fails on it, which is what CI wants.
112
63
 
113
- **The cadence is declarable.** `scheduling: { admissionDrainMs, inferenceTickMs, cancelSweepMs, pollCeilingMs }` in `app.config.ts`, each with a matching `VOLTRO_*` env var that overrides it the same ordering as `VOLTRO_TENANT_ISOLATION` over `tenancy.isolation`. They were internal constants, and a number the framework picks on a user's behalf belongs somewhere they can read it without reading our source. One resolver, called by both boot paths, so there is no second default to drift.
64
+ It probes ANONYMOUSLY on purpose. That is strictly weaker than scope-by-scope differentiation and strictly safer: the alternative puts credential minting into a command that can be pointed at production. Procedures declared `openAccess:` are skipped probing them would report every deliberately-public route as a finding and bury the real ones, which is the same signal-to-noise failure `kind: 'open'` was added to the wire to fix.
114
65
 
115
- **Why the rows never left.** A claim answers one question about one bucket and was already answered the moment the bucket passed. A winning claim now deletes that schedule's own predecessors, so the table's size is a small multiple of the number of schedules rather than a function of uptime. How far back it prunes scales with the caller's bucket width — a cron keeps ~68 minutes of them (its firings carry their own instant, so a stalled one can re-present an old bucket), a 250 ms task ~1 minute (it recomputes its bucket at tick time, so an old one is unreachable). Deleting too early is a double fire; that grace is the whole safety argument. The 24-hour retention sweep stays as the backstop for a schedule that was renamed or deleted, which the per-schedule prune can never revisit.
66
+ `fetchJson` gained an explicit `anonymous` option for this one caller; it is an opt-out, never a default, and both directions are pinned by a test a bearer attached here would make every result meaningless while still printing green.
116
67
 
117
- Where reactivity is absent a non-Postgres dialect with no broadcast brokera remote replica's enqueue produces no local event and the ceiling is the whole latency budget. `VOLTRO_POLL_CEILING_MS` is there for that case and documented as such.
118
- - **@voltro/logger** — The pretty log format now prints a nested `Error`'s `message`. It did not, and the JSON format did.
68
+ **Validated against a live app, and it took two corrections to get there.** The first version sent a readable request id, which the transport converts with `BigInt(id)` so every probe came back as a Defect before any guard ran, and every app looked broken. The second read a top-level `_tag` off an object while `POST /rpc` answers an ARRAY of envelopes, so a correctly-refused call scored as `admitted`. Both versions had a green unit suite, because the fixtures asserted the shape the code assumed. The fixtures are now copied from a real transcript.
69
+ - **@voltro/cli, @voltro/runtime** — `app.config.ts` gained `reactive: { deliveryConcurrency, rawReadTrackingLimit }` — the delivery-loop tunables were env-only, which left a number the framework picks on the project's behalf undeclarable in the one file that carries every other tunable.
119
70
 
120
- `Error.prototype.message` is non-enumerable, so `JSON.stringify(err)` emits the metadata and drops the message. `expandCauseForJson` has existed for a long time to solve exactly that and it was wired into `jsonFormat` only. The section heading above it said "(JSON path)", which was literally accurate.
71
+ Resolution stays inside the Dispatcher constructor (`resolveReactiveConfig`), so neither boot path can drift, and the env vars still win over the declared value: an operator acting on a running deployment outranks the project file. The threading itself is source-pinned across all three files (`dev.ts`, `serveCommand.ts`, `serveApi.ts`) because the serve side is a two-file relay and the union is where an option goes missing invisibly.
72
+ - **@voltro/cli, @voltro/database** — Data residency is DECLARABLE and wired. `tenancy.residency` in `app.config.ts` opens one store per servable region on both boot paths and routes every request to its tenant's home region — or refuses it.
121
73
 
122
- `voltro dev` prints the pretty format. What a consumer saw when their boot died on a saturated pooler:
74
+ The primitives have existed for two rounds (`setResidencyConfig`, `residentPlacement`, `bindResidentStore`), exported and tested, with **zero callers**. A user could reach them, but nothing in the framework did: there was no way to declare residency and no request ever consulted it. That gap was pinned by a test walking every workspace source, which went red on this change and asked for the module header to be corrected — it now names its consumers instead of asserting it has none, so a SECOND unreviewed caller still fails.
123
75
 
76
+ ```ts
77
+ tenancy: {
78
+ isolation: 'namespace',
79
+ residency: {
80
+ servableRegions: ['eu-west'],
81
+ regionUrlEnv: { 'eu-west': 'DB_URL_EU', 'us-east': 'DB_URL_US' },
82
+ homes: [{ tenantId: 'acme', region: 'eu-west' }],
83
+ },
84
+ }
124
85
  ```
125
- auto-migrate failed — aborting boot
126
- err={"failure":{"cause":{"length":117,…,"code":"XX000"},"message":"PgClient: Failed to connect"}}
127
- ```
128
-
129
- `length: 117` is the length of a message that is not there. Recovered by hand, it was `(EMAXCONNSESSION) max clients reached in session mode - max clients are limited to pool_size: 15` — the whole diagnosis in one sentence, naming the fix.
130
86
 
131
- Both the field tail and the plain-object cause branch expand now. The fix is in the formatter, not at the reporting call site: every `log.error('…', { err })` anywhere had the same hole.
132
- - **@voltro/cli** — Every `@voltro/*` package now exports its own `package.json`, so `require('@voltro/cli/package.json').version` works.
87
+ `regionUrlEnv` names an env VAR, not a URL a connection string is a secret and `app.config.ts` is committed. Everything else about a region's store (pool bounds, TLS, `search_path`, timeouts) is inherited from the primary connection, so a region cannot silently run with different limits than its deployment.
133
88
 
134
- It threw. Node has enforced this since 12: a package with an `exports` field exposes only what that field lists, and none of the 77 packages listed `"./package.json"`.
89
+ **Every failure is a refusal, never a fallback**, because a residency system that degrades to a default store violates residency at exactly the moment something is misconfigured. Unresolvable tenant, unmapped home, or a home region this deployment does not serve are all typed refusals; the last one names the region so a gateway can route it.
135
90
 
136
- Reported by a consumer for whom it was the instruction WE gave for settling whether a security command had been running on stale codeso the verification step for a security question could not run at all. Both the workspace `exports` and the shipped `publishConfig.exports` are fixed, and a guard sweeps every package so a new one cannot ship without it.
137
- - **@voltro/cli, @voltro/sql-postgres** — The `db pool:` boot line now counts the connections this process holds OUTSIDE the pool, and names them.
91
+ Four declarations are refused at BOOT rather than warned about: residency without `isolation: 'namespace'` (the region keeps regions apart, the namespace keeps tenants apartone without the other is not isolation), a servable region with no env-var name, one whose env var is unset, and a tenant mapped to two regions.
138
92
 
139
- It reported `max × replicas` and called that the connection count. A consumer sizing a per-pod budget against a pooler measured the gap:
93
+ Two boundaries worth knowing:
140
94
 
141
- > `LISTEN` läuft außerhalb von `dbMaxConnections` (eine pro Pod, gemessen sogar > 3). Der echte Bedarf ist `dbMaxConnections + 1`.
95
+ - `ctx.storeForTenant(id)` resolves residency for THAT tenant, not the caller's, so a handler acting on another tenant reaches that tenant's region or is refused. Background work (schedules, workflows) runs with no tenant and must use it — `ctx.store` there is the primary store. - A transaction is never re-routed. A caller-supplied store is used as given; it already went through residency to exist, and moving writes off the connection holding the lock is a worse failure than the one residency prevents.
142
96
 
143
- Their measurement was right and their conclusion was one short. The framework opens a standalone connection in three places, and a full deployment holds all three:
97
+ Homes resolve once at boot (an array, or a function reading your own table), so adding a tenant home needs a restart chosen over a cache with a staleness window on a decision whose whole value is that it is never wrong.
98
+ - **@voltro/runtime, @voltro/cli** — `voltro schedule backfill <name> --from <iso> --to <iso> [--yes] [--limit N]` and `POST /_voltro/inspect/schedules/:name/backfill` (WF-14) — fire every cron occurrence of a schedule over an explicit range, sequentially, each recorded against its own cron-derived `scheduledAt` with `trigger: 'manual'`. Fills the gap boot backfill (walks from the last recorded run only) and cluster-cron catch-up (capped at one day) leave open. Bounded and confirmable: above 25 occurrences it refuses without `--yes` (printing the count), above the per-request cap (default 1 000, `--limit` up to a hard ceiling of 10 000) it refuses outright, firing nothing — never a silent prefix. Wired on both boot paths through one shared hook.
99
+ - **@voltro/devtools-ui, @voltro/plugin-search** — The Search dashboard panel now RENDERS the drift surface REL-1 shipped server-side and no dashboard showed (the additive-JSON silent-drift shape the 4-layer rule exists for): per-index `dropped` / `pendingDrift` / `drifted` / last-drift badges, the repair queue itself (`GET /drift` — oldest first, with attempt counts and the engine's last error), and a **Resync now** action (`POST /resync`) gated on the new `canResyncSearch` capability (its own flag — a resync re-reads only the drifted rows; a reindex re-reads the whole table). Landed across all four layers in one change set: shared `SearchPage` + wire types + capability + EN/DE strings here; HTTP fetchers + page wiring in voltro-devtools; tenant-scoped `apps.inspectSearchDrift` / `apps.inspectSearchResync` proxies + hooks + page wiring in voltro-cloud (the indexes proxy schema carries the new fields as OPTIONAL, so a customer app from before the drift ledger still decodes). `search.query` also now carries an explicit access decision (`openAccess`, with the tenant-scoping rationale in source) instead of the undecided SEC-1 shape.
100
+ - **@voltro/cli** — Workflow wakes over the change stream (WF-8): on a fleet where remote changes reach the change spine (Postgres LISTEN/NOTIFY CDC — the common broker-less multi-replica deployment), a remote replica's `signal-sent` event, start context, or run transition now triggers an immediate, coalesced `pollStorage` on every replica, so cross-replica signal/step latency stops being bounded by the 10 s storage poll. Honest subset by design: the cluster engine has no per-run wake seam, so the change event wakes the poll early rather than replacing it — the poll tick stays the safety net. Local-origin changes never wake (a replica waking on its own recorder rows would be a poll storm). Wired by the same `makeWorkflowWake` builder on both boot paths.
101
+ - **@voltro/runtime, @voltro/workflow, @voltro/cli** — `ctx.workflows.start(name, payload, { at: Date })` — delayed one-off starts (WF-13). The start is parked as a durable `_voltro_workflow_pending` row (`mode: 'delayed'`) and fired by the coordinated drainer when `at` arrives, so it survives restarts and fires on whichever replica drains. At `at` it becomes an ordinary ARRIVAL: declared flow control (debounce, singleton, rateLimit, …) judges it as of that moment — `at` never bypasses a control. The handle reports `status: 'queued'` with `deferral: { mode: 'delayed', dueAt }`. An `at` in the past starts immediately; `{ at, wait: true }` is refused. `at` is an absolute instant by design (no `delay` spelling): a delay is ambiguous about its epoch and every queue system answers it differently, while an instant composes with the schedule/backfill surfaces.
102
+ - **@voltro/workflow, @voltro/cli** — `workflows: { recording: 'coarse' }` in `app.config.ts` (env override `VOLTRO_WORKFLOW_RECORDING`) — turns off the two fire-and-forget per-step writes to `_voltro_workflow_run_steps` (WF-10) for hot high-step workflows. Run rows, run events (signals/timers/cancels/stall reports) and the cluster engine's durable journal are unaffected — replay and redrive work exactly as before; the cost is an empty step timeline for runs recorded under coarse. Measured before it was built (`packages/cli/scripts/admission-throughput.mjs`): the recorder costs exactly 2 store writes per step, off the step's critical path — which is why the knob is a skip, not a batcher.
103
+ - **@voltro/workflow, @voltro/cli** — `workflow({ schedule })` — the workflow-side cron declaration (WF-12), with Temporal Schedules' overlap vocabulary about the RUN: `onOverlap: 'skip' | 'buffer' | 'cancelOther'`. Pure sugar over the shipped scheduler: at boot it lowers into a real schedule named `workflow:<name>` (same coordinated claims, run rows, Schedules panel, `voltro schedule` verbs). The synthesised firing awaits the workflow run to completion, which is what makes skip/buffer bind on the run's duration; `cancelOther` cancels only the still-running run this schedule itself started. The firing watchdog (`schedule.maxRuntime`) defaults to 24 h here. Cron and timezone are validated at definition time.
144
104
 
145
- | Process | Connection | When | |---|---|---| | api `voltro serve` | CDC `LISTEN` consumer | `changeStrategy: 'cdc'` | | web `voltro start` | ISR invalidator `LISTEN` | a page declares `cacheInvalidatesOn` | | web `voltro start` | postgres ISR cache client | `SSR_CACHE=postgres` |
105
+ ### Changed
146
106
 
147
- The third is not a `LISTEN`, which is why counting `LISTEN` rows in `pg_stat_activity` undercounts, and why `+1` could not have been documented as a constant: the count is per PROCESS and only the process knows what it armed.
107
+ - **@voltro/plugin-clickhouse** `clickhouseAnalytics` now BATCHES `track()` inserts by default (PERF-11) — 20 events / 5 s, plugin-posthog's conservative numbers — instead of one HTTP insert (and one MergeTree part) per event. What changes observably for an app that never set `batch`: a successful `track()` now means "buffered", not "ClickHouse accepted the row"; events become readable up to 5 s after they were tracked; a flush failure drops that batch with a warning (a hard crash loses whatever is still buffered — graceful shutdown drains via `dispose`). Opt OUT with `batch: false` to restore one immediate, confirmed insert per event; `batch: { maxSize, flushIntervalMs }` tunes the window. No compile break `batch` widened to `ClickhouseBatchOptions | false`, and the previous opt-in spelling keeps working (its defaults are now 20/5000 rather than 1000/5000).
108
+ - **@voltro/database, @voltro/cli, @voltro/workflow** — The migration advisory lock is now scoped to the configured schema (`DB_SCHEMA`) instead of one framework-wide constant. A postgres advisory lock is database-scoped and MySQL `GET_LOCK` is server-wide, so two apps sharing one database in different schemas used to serialize each other's migrations and defer each other's boot-time trigger repair — with a log line blaming "another instance". Now: postgres derives a stable 64-bit key from the schema name (FNV-1a 64 of `voltro_migration_lock:<schema>`, sign bit cleared; collisions across schemas are possible and only reintroduce serialization, never a race); mysql/mariadb/mssql suffix the lock NAME with the schema (hashed past MySQL's 64-char `GET_LOCK` cap). Every taker moved together in this change — the declarative applier, the file-based runner, the boot auto-migrate, the CLI's reactive-trigger boot repair, and the workflow cluster first-boot gate (its own distinct key, same derivation). On mysql/mariadb, where `GET_LOCK` is server-wide and `DB_SCHEMA` is not a connection pin, setting `DB_SCHEMA` to your database name is how two apps on one server un-share the lock.
148
109
 
149
- The line says `No connections outside the pool in this process` when there are nonesilence about it is what made "counted, zero" indistinguishable from "not counted". The `maxConnections` docstring, which promised `+1` as if it were the deployment's number, is corrected. Production-hardening docs (both languages) gain the table plus the `maxSurge` arithmetic a rolling update needs.
150
- - **@voltro/cli** — The retention sweep is registered on every dialectit was postgres-only, and silently.
110
+ **Rolling-deploy story.** An app WITHOUT `DB_SCHEMA` (or with `DB_SCHEMA=public`) keeps the EXACT pre-change lock key and nameold and new replicas contend on the same lock throughout the rollout; nothing to do. An app WITH a non-default `DB_SCHEMA` changes its lock key when it lands this version: during that one rollout window, old-generation and new-generation replicas do not mutually exclude their DDL. The boot auto-migrate DDL is idempotent (`IF NOT EXISTS`-shaped), so the practical exposure is the known postgres `CREATE TABLE IF NOT EXISTS` catalog race worst case one replica's boot fails and restarts. Avoid running `voltro db apply` concurrently with THAT rollout; after it, everything contends on the schema-scoped key.
111
+ - **@voltro/plugin-search** — `POST /reindex` now STREAMS the source table (keyset-paginated `streamTable`) and upserts one bounded page at a time instead of loading the whole table into memory the old shape was an OOM on exactly the tables big enough to need a reindex (PERF-12). The page size is a new tunable, `searchPlugin({ sync: { reindexBatchSize } })` (default 1000), and the `/indexes` panel reports it as part of the policy in force. `backfillIndex` keeps its plain-array signature for small explicit seeds. Additive surface only: a new optional `SearchSyncOptions` knob + a new `SYNC_DEFAULTS` key — no existing call site changes meaning.
112
+ - **@voltro/plugin-search** — Sync-stat counters no longer pay a read+CAS against the OLTP primary on EVERY indexed-table write (PERF-14). Counts buffer in memory and flush per window — `searchPlugin({ sync: { statsFlushIntervalMs } })` (default 5000 ms; `0` restores the per-event durable write) with an early flush at `statsFlushMaxBuffered` (default 1000) pending counts. Mirrors the runtime's api-key usage buffer, SHUTDOWN included: plugin deactivate drains the tail on both boot paths, so a graceful deploy loses nothing; a hard crash loses at most the current window of counters (never a change — the drift ledger stays the durable record). `GET /indexes` drains the buffer before reading, so the panel stays truthful mid-window. `StatsStore` gained a delta-applying `add` (the flush target); both shipped impls carry it and nothing consumes user-provided `StatsStore` implementations.
113
+ - **@voltro/database, @voltro/plugin-webhooks, @voltro/testing, @voltro/voltro, @voltro/workflow** — Golden churn from this round's signature WIDENINGS, classified per package:
151
114
 
152
- `wireRetentionSweep` opened with `if (dialect !== 'postgres') return`, so on mariadb, mysql, mssql and sqlite **none of its seven policies was registered, nothing was ever deleted, and the boot printed no armed-policies line** — so there was nothing to notice either. A consumer on MariaDB 11.8.8 measured it by reading the published bundle rather than their logs:
115
+ - **@voltro/database** — every migration entry point (`applySchema`, `runMigrate`, `runFrameworkBootstrap`, `applyNamespacedSchema`, `provisionTenantNamespace`, the lock functions) gained a trailing OPTIONAL parameter (`SchemaApplyOptions` / `MigrationLockScope`) for the schema-scoped lock and the dialect retry predicate. Every existing call compiles unchanged; omitting the parameter is exactly the old behavior. - **@voltro/workflow / @voltro/testing / @voltro/voltro** the same widenings re-exported through the aggregates, plus `PresenceWrite`-adjacent type surface already classified in this release's presence entry. - **@voltro/plugin-webhooks** — `deliverWebhookWorkflow`'s payload type inference had COLLAPSED to `AnyStructSchema | Struct<Fields>`, which made `execute`'s requirements `any` for every consumer: there was no type contract in force to break, only one that silently did not exist. It now infers the real payload struct. The export's only callers are the framework's own boot paths (it exists for cluster-runner registration); an app that passed a wrong-shaped payload under `any` now gets the compile error it should always have had — which is the fix, not collateral.
153
116
 
154
- ```
155
- _voltro_schedule_claims 109 520 rows 32.8 MB over 20 days
156
- _voltro_schedule_runs 17 507 rows 7.4 MB
157
- ```
117
+ ### Fixed
158
118
 
159
- Their seven `VOLTRO_*_TTL_HOURS` variables were inert read only inside the branch that never ran and two of them were already set in their Helm chart.
119
+ - **@voltro/cli, @voltro/protocol** — `voltro doctor`, the boot refusal, and `GuardSpec.resource`'s own doc comment now name the per-resource form of an access decision. All three listed two ways to decide and there are three.
160
120
 
161
- **The gate was aimed at the right thing and applied to the wrong scope.** What is postgres-specific is the fast DELETE (`"camelCase"` quoting, `DELETE RETURNING`), which is one branch of one function that has always had a portable fallback beside it. Gating the REGISTRATION on it turned a performance choice into a feature that does not exist. The dialect check now sits on the branch it describes.
121
+ A consumer with 565 undecided procedures set `security: { defaultDeny: false }` across their app, and their reasoning was correct at every step from what they were shown. Their authority is per-team a viewer in one team, an admin in another — so a subject-global `guards: [{ scope }]` would state a check they do not perform, and the boot refusal warns against exactly that ("reaching for a scope every caller already holds satisfies the gate, reads as protection, and enforces nothing"). `openAccess:` would be untrue. Both offered forms were rightly rejected, so they turned the gate off and kept enforcing in handlers.
162
122
 
163
- Two things came out with it:
123
+ The form that fits them — `guards: [{ action, resourceType, resource }]`, backed by `defineResourcePolicy` and a tuple source registered over their own tables — has shipped for several releases, is wired on both boot paths, fails closed without a resolver, and is documented under Authentication → Authorization. They looked: they read `GuardSpec.resource`, whose doc comment described the resolver as "a future ReBAC / `accessPolicy()` resolver". That sentence was written before the ReBAC path shipped and never updated, and it is the only thing a reader of that type has. A doc comment that says "future" about something built is not a small inaccuracy — it argued a careful team out of a security gate.
164
124
 
165
- - **The fallback deleted row by row.** Acceptable while the path was unreachable; against the reporter's backlog it is 20 000 round trips per sweep pass. It reads a bounded batch of ids and issues ONE set-based delete for them — still bounded, so the DELETE never grows to lock the whole backlog. - **Two tests asserted the defect as intended behaviour**, with reasoning that was internally consistent and rested on the premise that was itself the bug (*"the sweep is postgres-only, and announcing a delete that will not happen is the mirror image of the defect"*). Both are inverted now and run across all five dialects.
125
+ An enumeration inside a refusal is read as exhaustive, and the more careful the reader, the more thoroughly they act on it. `accessDecisionForms.test.ts` pins all three forms in all three places, including the `defaultDeny: false` branch an app that has already given up is precisely the audience that needs to learn there was a third option.
126
+ - **@voltro/protocol, @voltro/database, @voltro/runtime, @voltro/plugin-broadcast, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql** — A reactivity-channel publish now says where it came from — `origin: 'inline'`, because it happened in THIS process — instead of borrowing the `'injected'` stamp its transport seam applies by default. Two defects came out of that one mislabel, both silent:
166
127
 
167
- This is the third turn of the same screw, and the reporter's framing is the one to keep: we fixed *a standing delete that never introduces itself*, then shipped *one that introduces itself and does not run* and beside both of those sat one that silently did not exist.
168
- - **@voltro/cli** — `voltro db scan-credentials` no longer reports the framework's own redaction markers as credentials, and no longer claims a match was a *key*.
128
+ - **A channel published synchronously from inside a change listener never left the replica.** `plugin-broadcast` suppresses re-publishes while it is injecting, and the bracket was coarse: it dropped EVERY emission made in that window, not just the event it had injected. So `onChange` → `publishReactivity` woke the local node and no peer ever heard it — no error, no log. The one plugin doing cross-replica fan-out (presence) escaped only because it re-publishes from its own transport callback. The guard now suppresses by provenance, so a local publish made inside the bracket travels like any other. - **A replica could not tell its own channel publish from a peer's.** Both arrived `'injected'`, so a listener fanning a channel onward had nothing to key on and needed a boolean per channel to avoid an echo. `origin` answers it now.
169
129
 
170
- A consumer with correctly-redacting plugins got:
130
+ Two supporting changes, each with its own failure mode:
171
131
 
172
- ```
173
- ✗ _voltro_row_history.data — 69 of 149 row(s) match a credential-shaped key
174
- matched (rows per needle, may overlap): token (69)
175
- ```
132
+ - `origin` no longer survives the wire. It describes how an event reached THIS process, so the receiving replica strips what the sender serialised and stamps its own. Without this the guard fails OPEN — measured, an arrival still claiming `'inline'` amplified one publish into 163 events and killed the test worker. - The transport-origin stamp has one definition (`externalChangeEvent`, `@voltro/database`) instead of five hand copies across the memory store and the four dialect stores. `injectOriginParity.test.ts` fails if any store grows its own again — a store that hand-stamps would override a caller's stated origin, and the visible result is a channel that stops crossing replicas on that dialect only.
176
133
 
177
- All 69 rows were `"_omitted": ["token"]` `@voltro/plugin-versioning`'s record that a `.serverOnly().sensitive('secret')` column was deliberately left OUT of the snapshot. The scan matched the proof that nothing is stored there, called it a credential, and printed *purge them AND rotate the credentials* underneath.
134
+ `DataStore.injectExternalChange` keeps its shape: an event that states no origin is still stamped `'injected'`. A store passed to `plugin-broadcast` that does not stamp at all (the interface is structural) is detected by identity and falls back to the old coarse suppression rather than amplifying.
135
+ - **@voltro/cli, @voltro/devtools-ui** — `voltro check` no longer mistakes a deliberate `openAccess:` mutation for an unguarded one — and no longer mistakes it for a guarded one either. The manifest serialises the decision as a `kind: 'open'` guard entry carrying the reason string; `toInput` now translates it into `openAccess` on the graph procedure with `hasGuards: false` (nothing IS checked), and the `rbac/unguarded-mutation` rule skips a procedure whose author already "confirmed it is intentionally public" — the rule's own fix text. Previously the open entry was counted as a guard, so the finding disappeared for the wrong reason: the open mutation read as protected.
178
136
 
179
- Two changes. The headline says what the predicate does it is a substring match over the whole serialized column, so it finds a credential-shaped **name** anywhere in the value, which it always did. And each hit is now EXPLAINED: a bounded second pass (500 matched rows per target) reads them back in-process and separates a JSON **key** from a **redaction marker** (`_omitted`, `__redacted`). Values are never printed and never logged.
137
+ `@voltro/devtools-ui` gains the hand copy of the `SerialisedGuard` wire union (it is deliberately dependency-free, so it cannot import the CLI's), renders an access badge on the RPC page guard count, or "open access" with the reason in the tooltip and the copy is pinned from the owning side by `serialisedGuardParity.test.ts`, in the style of `migrationOpKindParity`.
138
+ - **@voltro/cli** — `ctx.query` in a loader rejects an `error` event instead of returning it as the query's rows.
180
139
 
181
- A target whose every matched row is a marker reports as explained and exits `0`. The bar is deliberately highevery matched row examined, every one a marker and nothing else. A capped read-back, one real key, or one row that will not parse as JSON keeps the target a finding and still exits `1`.
140
+ A consumer put a guard with an unheld scope on a query and called it over HTTP with a valid user JWT. The batch came back 200 with an `error` chunk carrying a `ScopeError` and an `Exit: Success` after it. Both are correct the transport worked and the guard worked but `buildLoaderQuery` unwrapped exactly ONE member of the three-member `subscriptionEvent` union and passed the other two through as data. The loader returned the error OBJECT, it went into the SSR seed, and the component called `.map()` on it: `TypeError: kept.map is not a function`, on 135 pages, for every user.
182
141
 
183
- The shape mattered more than the one key name: the more columns an app classifies correctly, the more markers it writes, and the redder the scan turned.
184
- - **@voltro/runtime, @voltro/cli** — `advisoryLock` scheduling no longer reads the whole `_voltro_schedule_claims` table to answer whether one claim row exists, and that table is now swept on the scale it fills.
142
+ The `.catch(() => null)` in our own documented loader pattern could not fire, because nothing was thrown. Neither could `seedPagePreloads`' `onError`, for the same reason. The same rejection over the live socket sets `error` and leaves data empty, so the two transports were saying different things about one event.
185
143
 
186
- The existence check ran `SELECT "id" FROM "_voltro_schedule_claims"` with no `WHERE` and no `LIMIT`, then filtered in JavaScript — twice per claim attempt (the fast path, and the re-read that separates "lost the race" from "the claims table is broken"), on every replica, for every schedule firing. A consumer measured ten concurrent copies of that scan holding every connection of a 15-slot pooler, with an SSR render behind them at **300 490 ms**. It is a primary-key lookup bounded to one row now (`id` *is* the claim key).
144
+ The unwrap is exhaustive now: `snapshot` yields its rows, `error` rejects with the typed error attached (as `cause` and `voltroError`, so an app can still branch on `ScopeError` rather than parse a string), and a first-event `delta` structurally impossible today throws naming itself a framework bug rather than handing a loader an id-keyed patch. `buildLoaderQuery` has one implementation shared by `voltro dev` and `voltro start`, so the fix covers the production path the report did not measure.
187
145
 
188
- The pool-acquire bound added in 0.32.0 turns that from a hang into an error; it does not stop the scan from filling the pool. Both are needed.
146
+ The HTTP 200 is unchanged, and the reporter is right that it should be: the batch transport did succeed.
147
+ - **@voltro/database, @voltro/cli** — The schema apply (`applySchema` / boot auto-migrate / framework bootstrap) now consults the dialect's own transient-failure predicate instead of dying on the first `SQLITE_BUSY`. The gap was located in `@voltro/sql-turso`: `busy_timeout` cannot retry the deferred-upgrade lock class, the store path already honoured the dialect's `retryFilter`, and the migration applier never consulted it — so a DDL statement that met the schema lock failed on the first attempt while every equivalent DML statement would have been retried. The CLI threads each loaded dialect's `retryFilter` through the new `SchemaApplyOptions`; retries are bounded (`VOLTRO_MIGRATION_DDL_RETRIES`, default 4, exponential backoff with jitter) and only ever re-run statements that are safe to re-run: per-statement on the per-operation dialects (sqlite/turso/mysql/mariadb, `IF NOT EXISTS`-shaped or covered by the duplicate-index tolerance), whole-transaction on postgres/mssql, whose retryable classes (deadlock victim, serialization failure) roll the transaction back cleanly. No `retryFilter` threaded means no retry — exactly the previous behavior. The declarative plan applier (`applyPlan`) is deliberately unchanged: its operations are not uniformly idempotent, and partial failure there is owned by the resume ledger.
148
+ - **@voltro/web, @voltro/client** — The web first load no longer ships `msgpackr` — 190.5 → 180.6 KB gz (−9.9 KB, 5.1% of the whole first load) for every app, measured on the zero-procedure fixture and re-pinned into `bundle-budget.json`.
189
149
 
190
- `VOLTRO_SCHEDULE_CLAIMS_TTL_HOURS` also defaults to **24 hours** instead of 30 days. The 30-day default was copied from the framework's history tables (`_voltro_schedule_runs` and friends), and a claim row is a lock ledger it answers a question about one firing instant and nothing reads yesterday's. At the 1 557 rows/hour that consumer measured, a 30-day window reaches ~1.1 million rows before the first one ages out. Raise it deliberately if you need to; the number to reason about is the longest a replica may be paused and still be trusted not to re-fire a bucket it already lost.
150
+ It shipped because `@effect/rpc`'s RpcSerialization module top-level-imports msgpackr while every Voltro path selects `layerJson`, and msgpackr declares no `sideEffects` flag so no bundler was allowed to drop it. The fix is a dependency patch adding `sideEffects: false`, declared at BOTH workspace roots (the @effect/cluster patch rule: a meta-root `pnpm install` must apply it too, or the two installs fight over node_modules). An app that genuinely calls `makeMsgPack` keeps the library the flag only permits dropping it when unused.
191
151
 
192
- The boot announcement can now express an age under a day (`older than 1h`); it previously rounded every TTL to whole days, so an operator setting one hour read their own policy back as `older than 0d`.
193
- - **@voltro/runtime** — A coordinated periodic task armed below one second now runs at the interval it was given.
152
+ The bundle-budget gate's slack floor is what keeps this fixed: the budget is re-pinned to the new number, so quietly re-inflating past it fails CI.
153
+ - **@voltro/cli, @voltro/database** — Under `tenantIsolation: 'namespace'`, a tenant's namespace is now PROVISIONED on its first use — schema DDL plus the `onTenantCreate` seed lifecycle, memoised per namespace per process.
194
154
 
195
- `scheduleCoordinated` floors the wall clock to its `intervalMs` and races on that instant; the claim key truncated it to second precision. A task at 250 ms therefore produced four bucket instants per second that collapsed to one key the first tick won and the other three were dropped as "lost the claim". Measured: 1 of 4.
155
+ Neither ever happened: `provisionTenantNamespace` documented as "the entry point the CLI / runtime use, eager at migrate time or lazily on first use" had zero callers in the entire codebase, `withNamespace` returns a pure view, and so a fresh tenant's first request died on "relation does not exist" while the `onTenantCreate` seeds (recorded as wired) fired only from tests that called the function directly. Found by the claimed-wirings checker the moment it learned to read active-voice claims.
196
156
 
197
- `voltro.ai.inference` is armed at 250 ms and was dispatching once per second, on every multi-replica deployment, with nothing above `warn` to say so.
157
+ Both boot paths build the provisioner from one builder; the namespace view's async methods await the memoised ensure (one resolved-promise await after the first settle), sync members stay sync, and the method classification is a DERIVED guard — a new `DataStore` method fails the test until someone decides whether it must await provisioning. A failed provision is surfaced and forgotten, never cached: one transient DDL failure must not become a permanently broken tenant on a replica. Verified end to end on live postgres, control included: the raw view still fails against a fresh namespace, the provisioned one creates the schema and fires the lifecycle exactly once.
198
158
 
199
- This is the defect the coordinator's own comment describes at minute precision (6-field crons firing once a minute), one decimal place down; that comment was written before `scheduleCoordinated` existed, and `scheduleCoordinated` is the caller that goes below a second.
159
+ Eager provisioning stays the app's move (call `provisionTenantNamespace` from a seed or startup over your own tenant table) the framework has no tenant registry to enumerate, and the docs now say so instead of implying otherwise.
160
+ - **@voltro/runtime, @voltro/cli, @voltro/testing** — `setRowFilter`'s registration moved from a module-local variable to a `globalThis` cell, and a scoped store that receives no filter while one is registered now throws instead of serving unfiltered rows.
200
161
 
201
- Milliseconds join the claim key only when non-zero, so every cron key is byte-identical to beforeload-bearing during a rolling deploy, where old and new replicas computing different keys for one firing would both win and double-fire.
162
+ A consumer measured four read paths returning every row of the tenant to every employee 19/19 contracts, 231/231 time-entry requests, 6/6 user settings, 4637/4637 shifts on both HTTP and WebSocket, with `row filter registered` in the boot log and 27 green tests. One of those paths was the only thing keeping `userSettings.update` safe, so every user could edit every other user's settings.
202
163
 
203
- Note the consequence for table size: a sub-second task now writes claim rows at its true rate. Bounded by `VOLTRO_SCHEDULE_CLAIMS_TTL_HOURS` (24 h), and `ai.tickIntervalMs` raises the interval if you want fewer.
164
+ Their exclusion work was exhaustive and right at every step: the load succeeded, `predicateFor` returned predicates for the right tables, nothing was `unconstrained`, no handler bypassed the store, and a `setRowFilter` + `makeTestContext` pair reproduced CORRECT filtering. That left them concluding the remaining variable was the store — memory in the green reproduction, postgres in the red deployment.
204
165
 
205
- ### Internal (no consumer-facing effect)
166
+ It is not the store, and the correction is the transferable part: their reproduction ran inside ONE module instance and production does not necessarily. `ROW_FILTER` was a module-local `let`, so an app's `*.startup.tsx` and the framework's serve pipeline write and read different variables whenever they hold different copies of `@voltro/runtime` — the serve bundle inlines the framework while app modules stay external, and strict pnpm resolves one version into two physical directories when two importers have different peer contexts. The pipeline reads `undefined`, correctly interprets it as "this app registered no filter", and serves everything.
206
167
 
207
- - **@voltro/cli** No separate consumer-facing note on purpose: this refines the per-needle breakdown described in the UNRELEASED 0.32.0 section, and that section which is what a reader will actually see carries the correction. Documenting it twice would describe one change as two.
168
+ `@voltro/database`'s `coreTablesRegistry` carries this exact fix with a comment describing this exact failure, for a value whose worst case is a crash at boot. This one's worst case is silent data exposure and it did not have it.
208
169
 
209
- The refinement: the per-needle counts OVERLAP and do not sum to the hit count (a row holding both a token and a secret is counted by both). The output line says so now, because two numbers printed under a total invite being added up, and a reader who adds them and gets more than the total loses confidence in the whole report.
170
+ The second half answers the reporter's second ask directly: a filter that cannot be applied must fail loudly rather than pass quietly. `undefined` and "we could not tell" had collapsed into one value, and the doc comment on that option already asserted they must not. Every deliberately unfiltered path system sweeps, `runAsSystem`, change-stream subscribers, the seeding store in a test now passes `NO_ROW_FILTER` explicitly, because "this app has no filter" and "this path is unfiltered on purpose" are different claims and only the second is a decision somebody made.
171
+ - **@voltro/cli** — A bare `voltro serve` under docker compose now drains on SIGTERM — in-flight requests complete against a fully-alive app, the listener refuses new work, live WebSockets are ended cleanly, and the process exits on its own, well inside `VOLTRO_SHUTDOWN_GRACE_MS`. No preStop hook or endpoint removal required. Two real defects closed (both measured against a live server): a single connected WebSocket wedged `nodeServer.close()` — node's `closeAllConnections()`/`closeIdleConnections()` cannot end an upgraded socket while `close()` still waits on it — so EVERY shutdown with a connected web client ran to the 10s deadline cut and the steps queued behind the close (the store's connection-pool close included) silently never ran; and the shutdown hook deactivated plugins and drained the analytics mirror BEFORE the request drain, so a request finishing during shutdown hit dead services and its writes were never mirrored. The drain is bounded: in-flight requests get 60% of the shutdown grace (floor 500ms), stragglers are then destroyed, and idle keep-alive sockets are swept continuously so a finished response never delays exit. The stale `serveApi` comment claiming `NodeRuntime.runMain` owns SIGTERM (and pointing at a k8s preStop hook as the fix) is rewritten to describe the drain that actually runs. Verify against a real serve with `node scripts/serve-drain-check.mjs`.