@voltro/cli 0.51.0 → 0.53.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 (233) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/THIRD-PARTY-NOTICES.md +8311 -3318
  3. package/dist/{agentsMd-D6yD7IQv.js → agentsMd-0l980yhL.js} +4 -1
  4. package/dist/agentsMd-SDDSkyl4.js +2 -0
  5. package/dist/{apiBuild-CPDHXF72.js → apiBuild-CaPfoWku.js} +11 -5
  6. package/dist/apiBuild-DHtLXYx9.js +2 -0
  7. package/dist/bin.js +1 -1
  8. package/dist/build-D-OnvNMf.js +843 -0
  9. package/dist/{checkCommand-DNuPiWMc.js → checkCommand-C5elt0tW.js} +92 -46
  10. package/dist/checkCommand-D2ZduVlh.js +2 -0
  11. package/dist/{cloudCmd-F4YJeqM3.js → cloudCmd-QUXh-b5w.js} +1 -1
  12. package/dist/codegen-BWpt3VgF.js +2 -0
  13. package/dist/{codegen-CrMXs4hb.js → codegen-FEk8AZHb.js} +2 -2
  14. package/dist/{codegenCommand-BNBHcNNj.js → codegenCommand-BOiWQ5hz.js} +12 -12
  15. package/dist/{codemodRunner-BDVixlSw.js → codemodRunner-BjtB2lq6.js} +691 -545
  16. package/dist/{commands-B1OiS9bX.js → commands-DyxAmhP0.js} +36 -36
  17. package/dist/{dashboardCommand-C-vvPY1B.js → dashboardCommand-BdKTyT13.js} +3 -3
  18. package/dist/{dataCommand-C1GxXW5q.js → dataCommand-Bab9X7s8.js} +27 -27
  19. package/dist/{dbCommand-If4Y1xQ-.js → dbCommand-06O2finM.js} +277 -236
  20. package/dist/dbCommand-B1EXBC6f.js +2 -0
  21. package/dist/{dev-kdAg9Q7l.js → dev-C6LGF4iY.js} +2998 -2379
  22. package/dist/dev-GjJWAYo2.js +3 -0
  23. package/dist/doctorCommand-B0hX0tdz.js +2 -0
  24. package/dist/{doctorCommand-nKmeW78u.js → doctorCommand-etMkflRc.js} +332 -220
  25. package/dist/{dormancyCommand-CY3wa_SW.js → dormancyCommand-UwZ1AZzB.js} +1 -1
  26. package/dist/{embeddingsCommand-BDLIgje_.js → embeddingsCommand-C70zWHwo.js} +1 -1
  27. package/dist/{envCommand-C6V_xVlT.js → envCommand-dSyKvRkM.js} +15 -15
  28. package/dist/{evolveCommand-D3c4DSfN.js → evolveCommand-CG0_ebO5.js} +2 -2
  29. package/dist/fileConventions-DASGEmj-.js +35 -0
  30. package/dist/{fileTaxonomy-CJfgOllU.js → fileTaxonomy-B7uxipWS.js} +55 -55
  31. package/dist/fontPipeline-LxIHa1vo.js +2 -0
  32. package/dist/fontPipeline-Tsh8kZfA.js +152 -0
  33. package/dist/frameworkTableAssembly-C_7Z-rMs.js +2 -0
  34. package/dist/{frameworkTableAssembly-BwJVEKLr.js → frameworkTableAssembly-DKx3ba3S.js} +5 -5
  35. package/dist/imagePipeline-B_GVJgm6.js +2 -0
  36. package/dist/imagePipeline-CBZmjT4i.js +127 -0
  37. package/dist/index.js +1 -1
  38. package/dist/{infoCommand-BnRFEF1o.js → infoCommand-_53iOc_j.js} +1 -1
  39. package/dist/{inspect-CtL_xTbu.js → inspect-Bd8-9wsi.js} +1 -1
  40. package/dist/inspect-CuoDInfZ.js +2 -0
  41. package/dist/{inspectGateHint-BjnFubmH.js → inspectGateHint-4LxkNtrz.js} +1 -1
  42. package/dist/inspectMetrics-CGF94puw.js +143 -0
  43. package/dist/manifestBuild-C4-J1-m_.js +2 -0
  44. package/dist/{manifestBuild-CuU1VrSm.js → manifestBuild-Cqgsx2bM.js} +1 -1
  45. package/dist/{metaCommands-CfRLra0s.js → metaCommands-Cn2oboG4.js} +9 -3
  46. package/dist/{migrate-DehuBakM.js → migrate-Cko9rswM.js} +2 -2
  47. package/dist/{pageConvention-cEiRxdab.js → pageConvention-C938S8oC.js} +1 -1
  48. package/dist/{privacyCommand-XejDMvmu.js → privacyCommand-DWTQMC6R.js} +2 -2
  49. package/dist/{probeCommand-C9gazU0H.js → probeCommand-DkGGLknv.js} +83 -24
  50. package/dist/{projectScaffold-mIX_DpSe.js → projectScaffold-B4dmTlwT.js} +1 -1
  51. package/dist/{projectScaffold-BIl97_E6.js → projectScaffold-EzlErR4E.js} +1 -1
  52. package/dist/{renderModeScan-D7J1B7Kw.js → renderModeScan-CUbOeOAg.js} +28 -11
  53. package/dist/{renderProfile-1OWWAAtx.js → renderProfile-CskIgAfn.js} +2 -2
  54. package/dist/{runtimeTrace-ZsBU7Tkx.js → runtimeTrace-c0APJz7E.js} +1 -1
  55. package/dist/{sdkgen-O4XqWOjM.js → sdkgen-BiQCgIEr.js} +1 -1
  56. package/dist/serveCommand-CueKQgzl.js +2443 -0
  57. package/dist/serveCommand-DsnrVN3U.js +2 -0
  58. package/dist/serveEntry.js +1 -1
  59. package/dist/start-BJzZLbt8.js +3 -0
  60. package/dist/start-ekPan8BT.js +1510 -0
  61. package/dist/startEntry.js +1 -1
  62. package/dist/{staticCommand-Dr2M6tpU.js → staticCommand-xlSL-IWk.js} +1 -1
  63. package/dist/{test-rXFq4S76.js → test-BWPQcRoB.js} +1 -1
  64. package/dist/updateCommand-Bqql_rsQ.js +2 -0
  65. package/dist/{updateCommand-Bs322Q78.js → updateCommand-C_8I8Rzo.js} +139 -115
  66. package/dist/webDev-C7jWJ5dX.js +2 -0
  67. package/dist/{webDev-B-ubQEMX.js → webDev-oczpugbx.js} +1767 -913
  68. package/dist/{webhooksCommand-FLYY9IXh.js → webhooksCommand-4SVPDjKg.js} +1 -1
  69. package/package.json +72 -18
  70. package/templates/AGENTS.core.md +11 -0
  71. package/templates/AGENTS.md +19 -6
  72. package/templates/agent-docs/_index.md +8 -6
  73. package/templates/agent-docs/_manifest.json +31 -15
  74. package/templates/agent-docs/ai.md +2 -2
  75. package/templates/agent-docs/authentication.md +1 -1
  76. package/templates/agent-docs/cli.md +97 -15
  77. package/templates/agent-docs/configuration.md +17 -0
  78. package/templates/agent-docs/data.md +680 -33
  79. package/templates/agent-docs/database/advancedqueries.md +7 -7
  80. package/templates/agent-docs/database/columntypes.md +2 -2
  81. package/templates/agent-docs/database/querying.md +1 -1
  82. package/templates/agent-docs/database/schema.md +2 -2
  83. package/templates/agent-docs/database/seedsdialects.md +2 -2
  84. package/templates/agent-docs/database/transactions.md +3 -3
  85. package/templates/agent-docs/deployment.md +30 -3
  86. package/templates/agent-docs/internationalization.md +2 -2
  87. package/templates/agent-docs/introduction.md +52 -0
  88. package/templates/agent-docs/local-first-mobile.md +132 -7
  89. package/templates/agent-docs/observability.md +2 -0
  90. package/templates/agent-docs/plugins/ai-flows.md +1 -1
  91. package/templates/agent-docs/plugins/audit.md +5 -5
  92. package/templates/agent-docs/plugins/auth.md +1 -1
  93. package/templates/agent-docs/plugins/cdc-out.md +2 -2
  94. package/templates/agent-docs/plugins/comments.md +142 -0
  95. package/templates/agent-docs/plugins/notifications.md +47 -4
  96. package/templates/agent-docs/plugins/presence.md +16 -3
  97. package/templates/agent-docs/plugins/prometheus.md +1 -1
  98. package/templates/agent-docs/plugins/queue.md +129 -0
  99. package/templates/agent-docs/plugins/{versioning.md → row-history.md} +22 -22
  100. package/templates/agent-docs/plugins/storage.md +2 -2
  101. package/templates/agent-docs/plugins.md +38 -12
  102. package/templates/agent-docs/reference.md +54 -5
  103. package/templates/agent-docs/routing.md +868 -50
  104. package/templates/agent-docs/schema-driven-ui.md +292 -5
  105. package/templates/agent-docs/security.md +125 -8
  106. package/templates/agent-docs/templates/apibackends.md +14 -14
  107. package/templates/agent-docs/templates/overview.md +1 -1
  108. package/templates/agent-docs/whats-new.md +171 -54
  109. package/templates/apps/api-ai/package.json +6 -7
  110. package/templates/apps/api-ai/tests/summarize.test.ts +1 -1
  111. package/templates/apps/api-auth/package.json +8 -8
  112. package/templates/apps/api-backend/package.json +7 -7
  113. package/templates/apps/api-backend-deactivation/package.json +7 -7
  114. package/templates/apps/api-backend-mail/package.json +8 -8
  115. package/templates/apps/api-backend-mariadb/package.json +9 -9
  116. package/templates/apps/api-backend-sqlite/package.json +8 -8
  117. package/templates/apps/api-backend-storage/package.json +8 -8
  118. package/templates/apps/api-cms/package.json +9 -10
  119. package/templates/apps/api-collab/package.json +8 -8
  120. package/templates/apps/api-data-advanced/package.json +8 -8
  121. package/templates/apps/api-durable/package.json +8 -8
  122. package/templates/apps/api-feature-flags/package.json +9 -9
  123. package/templates/apps/api-governance/package.json +8 -8
  124. package/templates/apps/api-kv/package.json +8 -8
  125. package/templates/apps/api-moderation/package.json +8 -8
  126. package/templates/apps/api-observability/package.json +8 -8
  127. package/templates/apps/api-ratelimit/package.json +8 -8
  128. package/templates/apps/api-rbac/package.json +8 -8
  129. package/templates/apps/api-rest/package.json +7 -7
  130. package/templates/apps/{api-versioning → api-row-history}/README.md +3 -3
  131. package/templates/apps/{api-versioning → api-row-history}/actions/documents.asOf.action.server.ts +1 -1
  132. package/templates/apps/{api-versioning → api-row-history}/actions/documents.history.action.server.ts +1 -1
  133. package/templates/apps/{api-versioning → api-row-history}/app.config.ts +3 -3
  134. package/templates/apps/{api-versioning → api-row-history}/database/schema.ts +1 -1
  135. package/templates/apps/{api-versioning → api-row-history}/mutations/documents.create.mutation.server.ts +1 -1
  136. package/templates/apps/{api-versioning → api-row-history}/mutations/documents.create.mutation.ts +1 -1
  137. package/templates/apps/{api-versioning → api-row-history}/mutations/documents.update.mutation.ts +1 -1
  138. package/templates/apps/{api-versioning → api-row-history}/package.json +8 -8
  139. package/templates/apps/api-row-history/template.json +6 -0
  140. package/templates/apps/{api-versioning → api-row-history}/tests/documents.create.test.ts +1 -1
  141. package/templates/apps/api-saas/app.config.ts +1 -0
  142. package/templates/apps/api-saas/package.json +10 -11
  143. package/templates/apps/api-saas-starter/package.json +10 -10
  144. package/templates/apps/api-search/package.json +8 -8
  145. package/templates/apps/api-status/package.json +8 -8
  146. package/templates/apps/api-webhooks/package.json +9 -9
  147. package/templates/apps/changelog/app.config.ts +26 -2
  148. package/templates/apps/changelog/content/releases/{0.1.0.mdx → v0-1-0.mdx} +0 -1
  149. package/templates/apps/changelog/content/releases/{0.2.0.mdx → v0-2-0.mdx} +0 -1
  150. package/templates/apps/changelog/package.json +8 -8
  151. package/templates/apps/changelog/src/collections/releases.collection.ts +48 -0
  152. package/templates/apps/changelog/src/globals.d.ts +1 -1
  153. package/templates/apps/changelog/src/locales/de.ts +1 -1
  154. package/templates/apps/changelog/src/locales/en.ts +1 -1
  155. package/templates/apps/changelog/src/pages/[locale]/[slug]/page.tsx +7 -4
  156. package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +13 -41
  157. package/templates/apps/changelog/src/pages/[slug]/page.test.tsx +26 -51
  158. package/templates/apps/changelog/src/pages/[slug]/page.tsx +21 -19
  159. package/templates/apps/changelog/src/pages/page.test.tsx +14 -37
  160. package/templates/apps/changelog/src/pages/page.tsx +18 -12
  161. package/templates/apps/edge-functions/package.json +2 -2
  162. package/templates/apps/frontend-admin/package.json +8 -8
  163. package/templates/apps/frontend-app/package.json +9 -9
  164. package/templates/apps/frontend-auth/package.json +8 -8
  165. package/templates/apps/frontend-blank/package.json +7 -7
  166. package/templates/apps/frontend-cms/package.json +9 -9
  167. package/templates/apps/frontend-collab/package.json +10 -10
  168. package/templates/apps/frontend-collab/src/pages/page.test.tsx +10 -9
  169. package/templates/apps/frontend-collab/src/pages/page.tsx +42 -65
  170. package/templates/apps/frontend-contact/package.json +7 -7
  171. package/templates/apps/frontend-dashboard/package.json +7 -7
  172. package/templates/apps/frontend-docs/content/docs/de/guides/first-page.md +4 -0
  173. package/templates/apps/frontend-docs/content/docs/de/intro/getting-started.md +4 -0
  174. package/templates/apps/frontend-docs/content/docs/en/guides/first-page.md +4 -0
  175. package/templates/apps/frontend-docs/content/docs/en/intro/getting-started.md +4 -0
  176. package/templates/apps/frontend-docs/package.json +8 -7
  177. package/templates/apps/frontend-docs/src/collections/docs.collection.ts +21 -0
  178. package/templates/apps/frontend-docs/src/locales/de.ts +0 -5
  179. package/templates/apps/frontend-docs/src/locales/en.ts +0 -5
  180. package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug]/page.tsx +17 -9
  181. package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +10 -5
  182. package/templates/apps/frontend-docs/src/pages/[locale]/page.tsx +8 -0
  183. package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.test.tsx +53 -15
  184. package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.tsx +27 -33
  185. package/templates/apps/frontend-docs/src/pages/page.test.tsx +17 -3
  186. package/templates/apps/frontend-docs/src/pages/page.tsx +16 -12
  187. package/templates/apps/frontend-i18n/package.json +6 -6
  188. package/templates/apps/frontend-landing/package.json +6 -7
  189. package/templates/apps/frontend-portal/package.json +8 -8
  190. package/templates/apps/frontend-saas/package.json +8 -8
  191. package/templates/apps/frontend-spa/package.json +7 -7
  192. package/templates/apps/frontend-ssr/package.json +7 -7
  193. package/templates/apps/frontend-ssr-api/package.json +8 -8
  194. package/templates/apps/frontend-static-blog/content/posts/cms-to-ssg.md +12 -0
  195. package/templates/apps/frontend-static-blog/content/posts/hello-static.md +12 -0
  196. package/templates/apps/frontend-static-blog/content/posts/islands-not-hydration.md +14 -0
  197. package/templates/apps/frontend-static-blog/package.json +8 -6
  198. package/templates/apps/frontend-static-blog/src/collections/posts.collection.ts +27 -0
  199. package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug]/page.tsx +7 -4
  200. package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +7 -1
  201. package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.test.tsx +19 -9
  202. package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.tsx +19 -21
  203. package/templates/apps/frontend-static-blog/src/pages/page.test.tsx +18 -5
  204. package/templates/apps/frontend-static-blog/src/pages/page.tsx +21 -14
  205. package/templates/apps/frontend-status/package.json +8 -8
  206. package/templates/apps/mobile-app/package.json +4 -4
  207. package/dist/agentsMd-Bu_XQgVf.js +0 -2
  208. package/dist/apiBuild-GDKuGOMV.js +0 -2
  209. package/dist/build-DETLZAFt.js +0 -752
  210. package/dist/checkCommand-CWcnDArJ.js +0 -2
  211. package/dist/codegen-DiMn2KkZ.js +0 -2
  212. package/dist/dbCommand-C27HIsGE.js +0 -2
  213. package/dist/dev-CK522MV5.js +0 -3
  214. package/dist/doctorCommand-BK4l18eG.js +0 -2
  215. package/dist/fileConventions-Cof68_BL.js +0 -33
  216. package/dist/frameworkTableAssembly-CVDB2hCq.js +0 -2
  217. package/dist/inspect-CuGDYES0.js +0 -2
  218. package/dist/inspectMetrics-CfdKLh6t.js +0 -72
  219. package/dist/manifestBuild-CPjhvM62.js +0 -2
  220. package/dist/serveCommand-BRnPCxVd.js +0 -2
  221. package/dist/serveCommand-DdiYNBBu.js +0 -2362
  222. package/dist/start-BLNmWkLa.js +0 -1154
  223. package/dist/start-Dzicuyw8.js +0 -3
  224. package/dist/updateCommand-eXB35SEv.js +0 -2
  225. package/dist/webDev-DposiF3j.js +0 -2
  226. package/templates/apps/api-versioning/template.json +0 -6
  227. package/templates/apps/changelog/scripts/generate-rss.mjs +0 -38
  228. package/templates/apps/changelog/src/lib/releases.ts +0 -21
  229. package/templates/apps/frontend-static-blog/src/content/posts.ts +0 -64
  230. /package/templates/apps/{api-versioning → api-row-history}/actions/documents.asOf.action.ts +0 -0
  231. /package/templates/apps/{api-versioning → api-row-history}/actions/documents.history.action.ts +0 -0
  232. /package/templates/apps/{api-versioning → api-row-history}/mutations/documents.update.mutation.server.ts +0 -0
  233. /package/templates/apps/{api-versioning → api-row-history}/tsconfig.json +0 -0
@@ -1,4 +1,4 @@
1
- # What's new in 0.51.0
1
+ # What's new in 0.53.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,100 +9,217 @@ BREAKING entries name a codemod; run `voltro update` to apply it.
9
9
 
10
10
  ### ⚠ BREAKING
11
11
 
12
- - **@voltro/data-transfer, @voltro/cli** — The asset counts reported references as if they were blobs. `_voltro_storage_refs` holds one row per reference and several rows legitimately name one key, so a capture of 57 rows over 16 keys wrote `count: 57` into the stamp beside an `assets/` directory holding 16 files, and the restore reported "57 blob(s) restored" while 16 objects appeared. Nothing was lost; what was lost is the ability to check. Anyone answering "are all the blobs there?" after a restore compared the stamp's number against one they counted and found a 3.5x gap that was not one.
12
+ - **@voltro/database, @voltro/runtime, @voltro/voltro** — `missingConflictColumns` takes the table `(table, conflictColumns, row)` and returns `{ column, reason: 'absent' | 'null' }` entries instead of bare names. The old signature structurally could not know which columns are DB-generated, so the upsert/insertIgnore guard built on it refused every write keyed on a stored generated column: a condition nobody can satisfy, since the database rejects an explicit value for a generated column and the framework's own stamping strips one. Such keys exist precisely to make a NULL-folding composite unique enforceable (`CASE WHEN ref IS NULL THEN 1 END` folding manual rows onto one value), so the guard refused exactly the schema shape it should protect — and the neighbouring `missingRequiredColumns` had carried the skip, with the reason written beside it, all along.
13
13
 
14
- A key named by several references is now fetched once rather than downloaded, hashed and discarded once per row, and the three numbers are stated separately: `references` (rows enumerated), `count` (distinct keys), `objects` (distinct sha256 bodies), with `totalBytes` and `objectBytes` beside them. The stamp's existing fields keep their names and now mean what a reader always took them for; the new ones are optional, so a stamp written before them still parses.
14
+ The guard's message now also separates the two bugs the flat list merged: an ABSENT key (`'x' is absent`) and a NULL one (`'x' is NULL NULL never matches a unique conflict target, so this write would always INSERT; use a plain insert for NULL-keyed rows, or make the column NOT NULL`). They have different fixes, and the shared word "missing" sent a reader hunting for an absent field that was present-but-NULL.
15
15
 
16
- **Breaking on one export.** `restoreAssetsFromCas` returns `{ count, objects }` instead of a bare `number` one number could not answer both questions, which is the defect. `count` is what the old value was, so the migration that changes nothing is `.count`.
16
+ Migration: pass the table definition (the value `missingRequiredColumns` already takes) and read `.column`/`.reason` off the results; `undefined` as the table means nothing can be recognised as generated.
17
17
 
18
- **`voltro update` carries you across this** — codemod `0.51.0/03_restore-assets-returns-counts`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.51.0).
19
- - **@voltro/i18n** — `<I18nProvider>` takes `timeZone` as its own prop, and `intlConfig` no longer accepts one.
18
+ **`voltro update` carries you across this** — codemod `0.53.0/01_conflict-columns-take-the-table`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.53.0).
19
+ - **@voltro/client, @voltro/ui, @voltro/cli, @voltro/web** — `useFormBinding` carries a WHOLE form now, on a per-field engine behind the facade — the engine is an implementation detail (no engine type in the public API, pinned by a contract test; production builds stub its devtools channel). The old surface is unchanged; everything new is additive:
20
20
 
21
- `intlConfig` exists to forward props to react-intl UNMODIFIED, and for every other member of `IntlConfig` that is the right shape. `timeZone` is the one member this package's own formatters read and they did not read it: `useFormatDate` built `Intl.DateTimeFormat` itself and took only the locale from the provider. So a zone passed through `intlConfig` configured `<T>`'s ICU dates and NOT the `useFormatDate()` beside them. Measured under one provider, one instant (`2026-08-24T23:30:00Z`), `locale: 'de'`, `intlConfig: { timeZone: 'Europe/Berlin' }`, process zone UTC: react-intl rendered `25.08.26, 01:30` and the hook rendered `24.08.26, 23:30`. A different hour, and a different day.
21
+ - **Nested values + sections.** A nested struct flattens into dotted fields (`address.city`) grouped under `section('address')` no more `custom` placeholder. The no-JS POST rebuilds the nested object from dotted input names, so both submit paths agree. - **Field arrays.** `array('entries')` push / insert / remove / move / swap + per-item field handles; arrays of structs carry item descriptors. - **Bound field handles.** `field('address.city')` returns value, setValue, onBlur, the display-gated error, touched/blurred/dirty, required, label, widget, options, and a11y props (`aria-invalid`, `aria-required`, `aria-describedby`) ready to spread onto any widget kit. - **Error timing a form can trust.** A form never opens with errors: a field reveals its error after ITS blur or after the first submit attempt, then live (`validate: { onChange: 'afterTouched' | 'always' | 'never' }`). `isValid`/`canSubmit` always tell the truth underneath. This changes VISIBLE behavior — errors used to appear only after submit; now blur reveals them earlier. - **`toInput` / `onSubmit` values mutation input.** Map form values to the wire input before validation, route input-schema issues back to form fields via `errorPath` (same-name automatic), or own the composed save with the mutation handle in hand — optimistic + server-error routing kept. - **One `form.state`**: isDirty (interaction-based), canSubmit, isSubmitting, isSubmitted, isSubmitSuccessful, submissionAttempts, errorCount, firstInvalidPath, pending, isLoading, submitError, data. - **`reset(nextDefaults)`** switches the edited record without a remount; `focusFirstInvalid()` moves focus to the first visible error. - **Per-field rendering.** `subscribe: 'fields'` + `useFormField(form, path)` a keystroke re-renders one field, not the page. - **Schema-declared structure.** `formField({ section, order, label, widget })` annotation, `description` help text, `Schema.Date` / `Schema.DateTimeUtc` → date/datetime widgets.
22
22
 
23
- The zone has to be a prop this package can see. It is validated once at the provider (an unusable zone a stale cookie, a typo, a runtime with a trimmed ICU — is dropped, because `Intl` THROWS on an unknown zone and `useFormatDate` catches, which would degrade every timestamp in the app to a raw `Date` string). It is what the new `useTimeZone()` reports. And it is what the framework fills per request.
23
+ The one compile-visible break: `WidgetKind` gained `'array'`, so an app registry typed as a TOTAL `Record<WidgetKind, Widget>` needs one new entry (the codemod note finds the shape). `reset` and `validateFields` only gained optional parameters.
24
+ - **@voltro/client** — Client-side validation messages are STRUCTURED now — stable ids with params (`validation.required`, `validation.minLength {min}`), read from the ParseIssue tree (schema ids + annotations), not effect's English developer text ("Expected string, actual undefined"). The binding renders them through a built-in en/de catalog (locale from `<html lang>`, override via `locale:`), and `messages: (id, params) => t(id, params)` wires an app's own i18n catalog in one line — the regexes apps laid over the developer texts can go. A `message` annotation may BE an id (`'validation.between|{"min":2,"max":50}'`), and a struct-level `filter` returning `[{ path, message }]` lands each issue at ITS field.
24
25
 
25
- **Migration:** `intlConfig={{ timeZone: 'Europe/Berlin' }}``timeZone="Europe/Berlin"`. The codemod does it, including the case where the zone was the bag's only member. An `intlConfig` naming a variable is reported by file and line rather than guessed at the property would otherwise stop being read with nothing red anywhere.
26
- - **@voltro/cli** — A native restore whose bookkeeping store would not open ran anyway, with no in-progress marker and no word about it. `nativeBookkeeping` was `try { … } catch { return undefined }`, and that `undefined` guarded every branch below — including the refusal for a marker that could not be written. So the failure removed the precaution AND the sentence that would have reported it missing, and `markerState` was never computed, defaulting to "held".
26
+ Two observable changes: `errors` keys are now the FULL dotted path (`'address.city'`, `'entries.0.startsAt'`) instead of collapsing to the top-level segment, and the display strings differ from the old developer text. `validateFields` additionally returns the raw `issues` array for widget kits that translate themselves.
27
27
 
28
- What it produced was worse than silence: a failed restore printed "This database is now in an unknown state and the next boot will REFUSE, by design" over a database with zero marker rows. The next boot did not refuse, and `voltro data clear-replace-marker` had nothing to clear. The trigger is not exotic — wrong credentials, an unreachable database, a missing env var, no `app.config.ts` from here and a restore is the operation you run against a target that is already unwell, so the guard fell away exactly when it was needed.
28
+ **`voltro update` carries you across this** codemod `0.53.0/03_validation-messages-are-ids`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.53.0).
29
29
 
30
- The reason now travels instead of being caught and dropped. A restore that cannot write the marker REFUSES and names which of the two reasons it was (the store would not open, or the table is not there); those were two separate refusals and are now one, because they are one decision for the operator. `--no-marker` is the deliberate way past it and warns every time. And the `recorded:` line no longer reports a local failure as a property of the target — it says where the failure was.
30
+ ### Added
31
31
 
32
- **This changes an exit code.** A restore that could not write the marker used to exit 0; it now exits 1. Two invocations are affected — the bookkeeping store will not open, or there is no `app.config.ts` from the working directory — and the second surprises people, because `restore` reads its target from the environment and so looks like it needs no project. It does not, for the dump; it needs one for the marker. `--no-marker` is the deliberate way through and warns every time. The note ships under `reach: 'beyond-source'` because the affected invocations live in cron entries, CI jobs and runbooks rather than in TypeScript.
32
+ - **@voltro/plugin-comments, @voltro/ui, @voltro/devtools-ui** `@voltro/plugin-comments` comment threads anchored to anything the app can name (an order, a document, a section anchor), live over the existing reactive engine: `comments.list` declares a `reactivityChannel` as its `source:`, every write publishes it, and a second client sees a new comment without a reload. No second push mechanism.
33
33
 
34
- **`voltro update` carries you across this** codemod `0.51.0/02_restore-refuses-without-marker`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.51.0).
34
+ Access FOLLOWS THE ANCHOR, fail-closed: the app declares `access.viaEntity` (guard delegation receives anchor, subject and the bound store, so the rule reads the entity's own row) or `access.scope`; with neither declared, every read and write refuses by name — a comments surface nobody opened serves nobody rather than everybody. A soft-deleted anchor is the same door: the guard cannot approve what it cannot read, so a stale mention notification finds "no longer available", not a leak.
35
35
 
36
- ### Added
36
+ Mentions are tenant-safe BY CONSTRUCTION, twice: the `resolveMentions` seam requires the calling subject in its signature and the plugin re-filters the result to the caller's tenant (opt-out `crossTenant`), and every mention is RE-validated at create time against the same resolver — a hand-crafted mention on a foreign tenant is dropped, never delivered. A validated mention sends through plugin-notifications when configured (preferences, quiet hours and digests apply — ten mentions in one window roll into ONE delivery, tested); without it, a log note and nothing else.
37
37
 
38
- - **@voltro/i18n, @voltro/cli, @voltro/voltro** `timeZone` in the web `app.config.ts` the zone every date/time formatter renders in, resolved per request and published so the client agrees.
38
+ Also in the box: replies (anchor-pinned — a reply cannot smuggle into a thread on a different anchor than the access check ran for), resolve/reopen, author-only edit, delete with a `comments:moderate` scope override cascading reactions, per-emoji reactions aggregated with `count` + `mine`, per-subject thread unread (`markRead`; your own comments are never unread for you), `useComments`/`useThread`/`useMentionSearch` hooks, the ejectable unstyled `<CommentsThread>` in `@voltro/ui`, and a Comments panel in both dashboards. Moderation is honestly opt-in (one plugin-moderation rule, documented — no "automatic" claim).
39
39
 
40
- A formatter is deterministic given the value, the locale, the zone and the clock. The locale already came from the provider and was already agreed across the hydration boundary the server publishes it as `<html lang>` and the client reads that attribute rather than `navigator.languages`, precisely because the browser's own answer can differ from what the server saw. The zone had no such source. `Intl` fell back to the zone of whichever runtime was formatting: the pod on the server (UTC on a container with no `TZ`), the viewer's machine in the browser. Every server-rendered timestamp was therefore a hydration mismatch waiting for a wide enough offset, and across midnight it was a different calendar day.
40
+ Proven over the real wire (`scripts/comments-e2e.mjs`, real `voltro serve` + postgres + signed session subjects): A comments B's ALREADY-OPEN subscription receives the new snapshot live; B's mention lands in the inbox (no self-notification); resolve at A arrives live at B; soft-deleted and missing anchors refuse; a cross-tenant mention delivers nothing. The docs site carries a LIVE demo (the real plugin against the docs demo backend). The declared limits are documented: attachments = storage-grant + URL, and the channel-wide reactivity granularity with the read-set work as the named narrowing.
41
+ - **@voltro/content, @voltro/cli, @voltro/changelog** — Content collections (plan 03): `@voltro/content` — file-based, schema-typed markdown content without installing a markdown dependency.
41
42
 
42
- timeZone: 'Europe/Berlin' // one zone for every viewer timeZone: 'viewer' // per request, from the `voltro:tz` cookie defaultTimeZone: 'UTC' // before the viewer's zone is known
43
+ `defineCollection` declares a folder (`content/<name>/**/*.md`) with an `effect/Schema` frontmatter schema in a `*.collection.ts` file. The isomorphic `getCollection`/`getEntry`: at build/SSR time the server reads the filesystem, decodes frontmatter (a violation FAILS the build naming the file), and renders markdown with dual-theme shiki; the build emits JSON artifacts under `dist/assets/content/…` that the CLIENT branch fetches on SPA navigations — no markdown engine, no highlighter, no content bodies in the browser bundle (proven by the fixture e2e's budget checks: 402 chunks → 9 after the split). Slugs come from the relative path; duplicates are build errors. Locale trees (`i18n: { locales, defaultLocale, missing }`) serve `de/` mirrors with per-collection fallback-or-404 policy. Rendered entries carry `headings[]` (depth/slug/text — the SAME ids stamped on the HTML, via one shared `extractHeadings`). `kind: 'data'` decodes `.json` files (authors.json). `reference('<collection>')` fields are validated by the build — a dangling reference names collection, entry, field and target. `config.feeds` builds RSS from a collection next to sitemap.xml and serves the same XML as a live dev route. `voltro dev` serves artifact shapes on demand and invalidates on `content/**` edits.
43
44
 
44
- Whatever it resolves to is stamped on the document as `<html data-voltro-tz>`, and the generated client entry reads that attribute. Both sides then format against one value which is the property that removes the mismatch, whether or not the value is the viewer's true zone: being wrong together is repairable after mount, being different is not.
45
+ `@voltro/changelog` now CONSUMES the seam (frontmatter + render via `@voltro/content/markdown`, `renderReleaseRss` via the generic `buildFeed`); unlabelled fences render plain instead of guessing `ts`. An unexpected static-loader throw at build time now FAILS the build instead of shipping an empty page (it shipped a whole docs site as 646 empty pages under exit 0). The blog/docs/changelog templates run on collections; the docs site migrated with a script-proven equivalence over all 646 pages × both locales.
46
+ - **@voltro/local-first, @voltro/database, @voltro/runtime, @voltro/protocol, @voltro/client, @voltro/cli, @voltro/plugin-row-history, @voltro/voltro** — CRDT beyond text: `crdtDoc()` stores a whole collaborative document as a column (same storage and doc-agnostic authoritative server merge as `crdtText()`, which stays as the plain-text specialisation), and `@voltro/local-first/editor` ships `useCrdtEditor` — a Tiptap binding (StarterKit + Collaboration + CollaborationCaret, all MIT, fully self-hosted; the paid Tiptap Cloud features are deliberately unused) over the new `CrdtDocHandle` (`createDoc`: the raw Y.Doc for the binding, `stateVector`/`encodeUpdateSince`, `onUpdate`, and `encodeAnchor`/`resolveAnchor` — the stable-position primitive inline comments pin threads with).
45
47
 
46
- Under `'viewer'` the framework injects a script that seeds `voltro:tz` from the browser when the cookie is absent, so the server renders in the viewer's zone from the second request with no login. It never overwrites an existing value the APP is the authoritative writer, at login, from the zone it holds for the signed-in user (`TIMEZONE_COOKIE` and `isSupportedTimeZone` are exported for that). Unset, nothing changes: each runtime keeps using its own zone, and `useTimeZone()` returns `undefined` to say so.
48
+ Wire amplification is fixed in BOTH directions. Upstream, offline edits coalesce per cell in the durable queue (1000 keystrokes drain as O(1) pushes) and a client push is an incremental update. Downstream, the new `mergeCells` patch op carries per-column incremental updates: the dispatcher diffs CRDT cells against the subscriber's previous state vector and the client folds them through `crdtMergeCell` a one-character edit against a 100 KB document measured under 1 KB on the subscription wire, no op in the delta carrying the full blob.
47
49
 
48
- This is the RENDER zone. The server-side compute zone what `startOfDay` resolves against inside a handler is still `@voltro/datetime/context`'s seam, unwired.
50
+ Fold atomicity is pinned in layers in the one store wrapper: a per-row in-process mutex serialises concurrent folds completely on a single node, a verify-and-refold pass heals cross-replica interleaves, and the residual multi-replica window is a stated limit (descriptor-level `FOR UPDATE` is the named next step). Storage stays bounded: a fold's result soft-compacts past `VOLTRO_CRDT_COMPACT_MAX_BYTES` (default 512 KiB) without breaking the merge lineage; `rebaseText` is the explicit hard reset — a new epoch subscribers receive as a fresh snapshot.
49
51
 
50
- **`apiSurface: compatible`** covers the two golden lines that moved, and they are the same change twice: `makeSsgWrap`'s returned wrapper, and the `wraps` record on the SSG shell input, each gained an OPTIONAL second parameter (the per-request zone + render instant; the wrapper is built once per locale, so they cannot live in the factory). A function with an optional extra parameter is assignable wherever the one-parameter type was expected, so no call site that compiled stops compiling and both are the framework's SSG bridge, documented as never imported by app code. Everything else this release adds to these packages is a pure addition; the one genuine break in `@voltro/i18n` is the `timeZone` prop, which has its own entry and its own codemod.
52
+ The capture paths know CRDT columns now: undo capture strips them from update images and skips crdt-only updates entirely (client-side doc undo is the editor's Y.UndoManager), row history excludes them the same way (document version history is named snapshots taken BEFORE compaction), and both exclusions keep whole images on DELETE. Exposure rules are declaration rules: `.serverOnly()` on a CRDT column throws (a doc clients write but never read cannot be collaborated on); `.encrypted()` is the documented online-only decision. Carets ride a `delivery: 'latest'` event via `attachAwarenessBridge` one member's state per envelope, measured far inside the event cap, never the aggregated room deliberately NOT presence metadata, whose value-compare push would make every caret move a "real" change.
53
+ - **@voltro/protocol, @voltro/runtime** — A write target declares its many-to-many relations now — and the framework writes the junction in the SAME transaction:
51
54
 
52
- ### Changed
55
+ ```ts
56
+ target: {
57
+ table: 'employees', op: 'update',
58
+ relations: { assignedStores: 'employee_assigned_stores' },
59
+ }
60
+ ```
53
61
 
54
- - **@voltro/cli**A `BREAKING` entry's changelog footer now tells a reader who pins versions by hand how to print the codemod's note without upgrading anything:
62
+ After the executor succeeds, `input.assignedStores` is reconciled against the junction through the diff-based link writer inserted, deleted, and unchanged rows are exactly the diff, so reactive subscriptions on the junction see one change per changed row. The link writes go through `ctx.store`: undo capture and cross-table rules see them, and a failure rolls the whole mutation back. Semantics pinned by test: an ABSENT input field touches nothing (absent ≠ empty), `[]` is the explicit clear, a non-array refuses by field name, the row id is `output.id` else `input.id`.
55
63
 
56
- voltro update --codemods-only --from <your current version> --dry-run
64
+ Underneath sits the new `store.relationLinks(junction, table, id)` — the existing `links()` with its anchor COLUMN derived from the junction's `reference()` targets; a self-junction is refused by name, never guessed.
65
+ - **@voltro/runtime, @voltro/protocol, @voltro/client, @voltro/cli, @voltro/voltro** — Delta-resume for subscriptions: a client that reconnects inside the resume window no longer pays for a full snapshot per query. The re-subscribe presents the last materialised revision in the per-call `voltro-resume-from` header (the same surface the idempotency key rides, read by the ONE shared auth-middleware builder so both boot paths agree), and the server — which keeps a resumable subscription alive server-side for the window after a disconnect, its emits recorded into a bounded per-identity delta ring — replays only the missed deltas and re-attaches the stream on the SAME revision line. The wire signal is the first event's tag: `delta` means resumed, `snapshot` means reset — no schema change.
57
66
 
58
- The footer used to stop at the codemod's id, which is enough for anyone who runs `voltro update` and nothing at all for anyone who does not. A deployment said so plainly: they pin every `@voltro/*` version from their own container scripts, have never run the command, and `CHANGELOG.md` out of the tarball is the only channel anything reaches them through. So a note deliberately filed under an unreached version our one mechanism for correcting guidance that can no longer be corrected in place reached that population not at all, and the id told them a fix existed without telling them what it was.
67
+ The failure direction is fixed everywhere: a wrong snapshot costs bytes, a wrong replay would leak rows, so every doubtful case answers with a fresh snapshot. Concretely: the ring is keyed by query + canonical input + subject + tenant (a login/logout/tenant-switch between disconnect and resume simply never finds it); the per-delivery guard re-check keeps running on the detached subscription and a revocation while offline drops the retained history (plus one more re-check at the adoption boundary); row-filtered apps and computed queries are excluded from resume entirely; a non-chaining or out-of-window revision falls back to snapshot. Replayed deltas may coalesce exactly as slow-consumer updates do.
59
68
 
60
- No new surface: every flag in that invocation is already parsed, which is what lets `check-message-apis.mjs` verify the line rather than trust it.
61
- - **@voltro/cli** — Two fingerprint labels now say what they compare.
69
+ `@voltro/client` participates automatically: the reconnect-seeded cache keeps its rows AND revision, sends the header on the re-subscribe, applies a resumed delta onto the held base with no snapshot round-trip, and treats a snapshot-first stream as the reset it already knew. Tunables ride the shared resolver both boot paths call: `reactive.resume.windowMs` (default 60 s, env `VOLTRO_REACTIVE_RESUME_WINDOW_MS`) and `reactive.resume.maxDeltas` (default 256, env `VOLTRO_REACTIVE_RESUME_MAX_DELTAS`). Proven end-to-end against a real `voltro serve`: kill a live subscriber mid-stream, write while it is gone, resume — first event is a delta past the held revision, a post-resume write reaches the adopted stream live, a headerless control gets a snapshot, and past the window the same header gets a snapshot again.
70
+ - **@voltro/cli, @voltro/web** — Font pipeline (plan 12). Declare local font files once (`fonts:` in the web `app.config.ts`) and get content-hashed self-hosting, `@font-face` with `font-display`, a SIZE-ADJUSTED fallback face (real metrics read via fontkit, capsize formula against Arial/Times — the swap moves nothing, CLS ≈ 0), a `<link rel="preload">` in the shell head, and opt-in unicode-range subsetting (`subsets: ['latin', 'latin-ext']` via subset-font, declared with matching `unicode-range`). Multiple weights/styles per family and variable ranges (`weight: '100 900'`) are first-class. `localFont('Inter')` in `@voltro/web` maps the declared family to its CSS variable/stack.
62
71
 
63
- The restore's skew warning said the backup's schema "differs from what this code declares". It does not: the value it compares against is the TARGET database's live schema, read by introspection at restore time. Bringing a target to the backup's shape makes the warning disappear while the declared fingerprint is a third value entirely, which is how the mislabel was caught. The comparison is the useful one and is unchanged; the sentence sent readers looking for a code change where a database differed.
72
+ ONE memoized build feeds every surface: `writeEntryFiles` bakes CSS + preloads into the generated shell (served identically by dev, static prerender, SSR streaming and `voltro start`), the dev server answers the hashed files from the same memo, `voltro build` writes them into `dist/assets/fonts` the shell's URLs and the files cannot disagree.
64
73
 
65
- `voltro db plan` prints `fingerprint: live · declared …` instead of `from to …`, plus a line saying the two are not meant to match. A hash of a live database never equals the hash of the declaration it came from introspection cannot recover generated expressions, `maxLength` or sensitivity markers which is why `db drift` keeps a separate live baseline. Printed as `from to`, `0 operations` under two differing hashes read as a contradiction.
74
+ No font CDN request ever leaves a visitor's browser the GDPR argument the docs carry (LG München), proven by e2e: a real chromium loads the page with ZERO foreign-host requests. Full e2e (`scripts/font-pipeline-e2e.mjs`): hashed woff2 in dist, subset measurably smaller than the source, @font-face + fallback face + preload in the built HTML, dev parity, browser network assertion. Deliberately NOT built: a Google-Fonts download helper (license terms are per-family the manual path is documented).
66
75
 
67
- ### Fixed
76
+ fontkit + subset-font ship as optional dependencies of @voltro/cli (script-free, verified — the plan-11 decision inherited); without them fonts still self-host and the metrics/subset halves degrade with one named warning each, plus a `voltro doctor` rule naming which half is missing.
77
+ - **@voltro/client, @voltro/ui** — The form contract, made seamless where it still had seams:
68
78
 
69
- - **@voltro/cli** — `voltro check` reported a reactivity CHANNEL as a missing table, at `error` severity so it set the exit code:
79
+ - **App-wide message wiring.** `<ValidationMessagesProvider messages={(id, params) => t(id, params)}>` once at the root resolves every form's schema ids AND server ids (`ctx.validation.fail`) through the app's i18n catalog the per-form `messages:` option still wins, `undefined` falls through per id. - **`toInput` is compiler-checked.** The typed `useFormBinding` has two shapes now: without `toInput`, form values ARE the mutation input; with it, the form gets its own `Values` shape and the mapper's return is checked against the mutation's input — a mapping that stops producing the wire shape is a type error. - **`<AutoForm>` renders the structure the schema declares.** Nested structs become real `<fieldset>` sections with legends; widget props come from the FIELD HANDLE, which fixes a real defect the audit found — a dotted field's value was read as a flat property, so nested inputs rendered permanently empty. Widgets receive `onBlur` (all built-ins forward it), so the reveal-on-blur timing works in AutoForm exactly as in the headless binding; `reference` reaches registry widgets for query-bound pickers. - **Proven over the real wire** (`scripts/forms-e2e.mjs`, real `voltro serve` boot): a `ctx.validation.fail` refusal arrives as a TYPED `ValidationError` with its field and message id, and a target's declared `relations:` reconciles the junction end to end — set, diff, absent ≠ empty, explicit clear — with an executor that never touches the junction.
80
+ - **@voltro/cli, @voltro/runtime** — The opt-in gRPC surface — an external client generated from the framework-emitted `.proto` calls a named Voltro procedure: unary for mutations/actions, server-streaming (live current-snapshot frames) for queries. Nothing re-implements the wire semantics: a gRPC call runs the SAME bound runner every other surface uses, so guards, the plugin interceptor chain (order proven side-by-side against a socket call in the e2e) and typed errors behave identically.
70
81
 
71
- error reference/dangling-source query(presence.list) reads table 'channel:presence' which does not exist fix: declare a 'channel:presence.entity.ts' table or fix the query's source
82
+ `app.config.ts` `grpc: { port, procedures: [tags], tls? }` — nothing exposed by default, every tag named, a phantom tag refuses the boot. The `.proto` comes from the procedures' own `effect/Schema` via a checked-in `grpc.manifest.json` whose FIELD NUMBERS are append-only: an inserted field never renumbers its neighbours, a deleted field's number goes `reserved` (emitted into the proto), and reusing a reserved number is a codegen error — the one gRPC trap that silently corrupts old clients. proto3 presence maps `Schema.optional` AND `NullOr` to the `optional` keyword (absent and null are one wire state, documented); the unmappable (shape unions, tuples, recursion, free-form objects) is a loud per-procedure error naming the schema path.
72
83
 
73
- A `source:` entry is a table name OR a channel's routing key (`channel:<name>`), and every rule resolved entries against the table set. The advice cannot be followed a channel exists precisely because no table is meant and because it is an error rather than a warning, `voltro check` could not be a CI gate for any app that uses a channel. That includes an app whose only channel comes from `@voltro/plugin-presence`, whose own `presence.list` declares one: a first-party feature meeting a rule that did not know about it, inside a first-party plugin.
84
+ The status table is COMPLETE against the wire error union, with the two non-failures distinguishable in trailers: `ScopeError` `PERMISSION_DENIED` (or `UNAUTHENTICATED` for a credential-less caller), schema-invalid input `INVALID_ARGUMENT` (the bridge decodes the proto-deserialized request against the descriptor's input schema proto3 suppresses defaults, so skipping that decode fails much later as a DB constraint), `BusinessRuleViolation` `FAILED_PRECONDITION` + `voltro-error: rule`, and a PENDING `requiresApproval` a flow outcome, not a failure `FAILED_PRECONDITION` + `voltro-pending: approval` + `voltro-approval-id`.
74
85
 
75
- Channels are filtered in the ONE helper every table rule reads, rather than at each rule, because a per-rule filter is how the next rule joins without one. The same cause was live one rule over: `observed/declared-but-unobserved` reported "declares source 'channel:presence' but never read it while running" for every exercised procedure that declares a channel. Both are covered, each with a negative control a filter that dropped the whole source list would have silenced the rules instead of narrowing them.
76
- - **@voltro/data-transfer** — A native dump no longer carries `_voltro_data_transfers`, for the same reason it stopped carrying the in-progress marker one release ago. The restore opens its own run row there BEFORE the tool runs; the dump then dropped the table mid-flight, and the update recording the outcome wrote into a table that no longer held the row. Measured downstream: after a deliberately failed native restore, `voltro data transfers` showed no restore at all — only the `backup` row the dump had carried over from the SOURCE database. The command that answers "did the restore finish" could not see the run asking the question.
86
+ Deadlines INTERRUPT the work: `grpc-timeout` aborts the executor's fiber through the new `ServeRequestContext.signal` (honoured at the one place the executor effect runs to a promise), pinned by an e2e where a 300ms deadline on a 2s action answers `DEADLINE_EXCEEDED` and the post-sleep write never lands. Streaming rides the dispatcher subscription binding (per-delivery guard re-check included) with grpc-js write backpressure frames coalesce to the latest snapshot instead of buffering unboundedly. `grpc.health.v1` + server reflection mount automatically; the gRPC packages are script-free optional dependencies (Apache-2.0), and a configured block with them missing refuses the boot by name. Also fixed on the way: the query subscriber's error events now keep a tagged error's `_tag` (it was collapsed to `{ message }`, blinding SSE consumers and the gRPC mapper alike).
77
87
 
78
- Exactly two tables are excluded and the line is deliberate: a native restore into the same deployment should bring the migration ledger, the stored plans, the CDC offsets and the schedule claims, because they describe the data being restored. These two describe the RESTORE, and a record of an operation must not be overwritten by the operation it records. Covered per dialect against real servers and real vendor tools, including a non-vacuity check that a table which SHOULD travel still does.
79
- - **@voltro/i18n, @voltro/cli** — `useRelativeTime` used `Date.now()` as its base, which under SSR is two different numbers. The server rendered at T and wrote "3 minutes ago" into the HTML; the browser hydrated at T+Δ and rendered "4 minutes ago" whenever a unit boundary fell in the gap. The gap is network latency, so it reproduced on a slow connection and never on the developer's machine, and it had nothing to do with timezones a correctly zoned app hit it just the same.
88
+ Declared v1 limits, with alternatives: no client/bidi streaming, no gRPC-Web (browsers use the framework's subscription protocol), no Connect protocol (REST/OpenAPI projection is the answer there), and `*.stream.ts` procedures are not exposable.
89
+ - **@voltro/cli, @voltro/web** — Build-time image pipeline (plan 11). `import hero from './hero.jpg?image'` turns a static asset into an `OptimizedImageAsset`: every ladder width up to the intrinsic width encoded as AVIF + WebP plus a same-family fallback, hashed into `dist/assets/`, with intrinsic width/height and a 16px blur data URI. `<Image src={hero}>` renders a `<picture>` with per-format sources dimensions and blur inferred, `placeholder="blur"` the default. The suffix is an explicit opt-in: bare image imports keep Vite's URL semantics untouched.
80
90
 
81
- The server states its render instant (`<html data-voltro-now>`, `renderedAt` on the provider), the first client render uses that same number, and the clock goes live once hydration commits. Server markup and hydration markup are therefore identical BY CONSTRUCTION the property `await.tsx` and `deferred.ts` already hold, rather than `suppressHydrationWarning`, which would hide a real mismatch along with this one. An explicit `{ now }` still wins.
91
+ Transforms run through a persistent cache (`.framework/image-cache/`, bounded concurrency) the second build re-encodes nothing (proven: `scripts/image-pipeline-e2e.mjs` asserts zero cache-file rewrites on build two, plus `<picture>`/srcSet/blur/dimensions in the prerendered HTML and a real chromium decoding a transformed WebP from the dev endpoint). In dev, `/_voltro/image/<assetId>` transforms on demand and answers ONLY for manifest-registered assets; `voltro start` serves build artifacts with no transform endpoint at all (deliberate no production transform-DoS surface).
82
92
 
83
- The mount state lives in the provider, not in the hook: a table of ten thousand rows would otherwise pay a state hook and a passive effect each to learn one fact that is true for the whole document. An app that never renders on the server publishes no stamp and takes no second render pass.
84
- - **@voltro/cli** — `closeNativeRun` writes the transfer row BACK when the restore's own artefact dropped the table it lives in, instead of issuing an `UPDATE` that matches nothing and returning happily. Excluding `_voltro_data_transfers` from our own dumps shortens that window; it does nothing for a dump taken before that change, for a hand-made one, or for mssql and sqlite, whose restores have no per-table exclusion at all. The write-back covers every dialect and every artefact, which is why it is the rule and the exclusion is the optimisation.
93
+ sharp ships as an optional dependency of @voltro/cli — auto-available, install-failure-tolerant, and script-free since 0.33 (prebuilds ride `@img/*` platform packages, so pnpm 10's build-approval gate does not apply; measured, correcting the plan's assumption). Without a working sharp the pipeline serves originals with ONE loud warning naming the fix, and `voltro doctor` distinguishes "not installed" from "installed but platform binary missing" (the omit-optional install). Tunables: `images.{formats,quality}` in the web `app.config.ts`; per-`<Image>` `quality` flows into the CDN loader seam, which stays the answer for dynamic/remote `src`.
85
94
 
86
- The row is read back rather than trusted an update that matched nothing is indistinguishable from one that matched and a read that itself fails writes nothing, because a duplicate row invented on a guess is its own defect in a history somebody reads under pressure.
87
- - **@voltro/cli** — A prerendered page shipped the shell's baked `lang="en"` whatever locale it was rendered in.
95
+ `apiSurface: compatible`additive: `OptimizedImageAsset` + `ImageLoader.quality` + the `quality` prop on `@voltro/web`, the `images` config block, and the new CLI modules.
96
+ - **@voltro/web** — Intercepting routes (plan 22): the modal-with-URL pattern. A page exporting `intercept: { from: '/photos' }` renders as an OVERLAY above the still-mounted origin page on a soft navigation from a `from` route, standalone on a hard load (and on soft navigation from anywhere else), and closes on Back — with the background's mounted state, scroll and subscriptions untouched.
88
97
 
89
- `voltro dev` and `voltro start` both set `<html lang>` per request; the prerender never did. So a `/de/...` artefact rendered with the German catalog, handed `locale: 'de'` in its `meta` served `<html lang="en">`. That attribute is what a screen reader pronounces in, what Chrome offers to translate FROM, and what hyphenation uses, so the failure was silent to whoever shipped it and loud only to the people it excluded. The same shape as the 0.30.0 cookie-name drift, one document path over.
98
+ The architecture is the surgical variant of the dual-tree model: the router's single committed chain becomes the BACKGROUND tree (its render pathname held on the bottom of a background stack persisted in `history.state.__vwebBg`), and each overlay level is a second, narrow render path own match, own page-loader state (through the SAME LoaderCache key a standalone visit and prefetch warm), own RouterContext provider. Nested modals stack; a replace inside a modal (a `useSetSearchParams` tweak) carries the stack forward instead of wiping it; a hard load IGNORES a surviving stack, because the server rendered standalone and hydration must match.
90
99
 
91
- It surfaced while giving the zone somewhere to travel: `<html lang>` was set by four hand-written copies of one `.replace(/<html…/)` and by nothing in the prerender, and adding a second attribute to that arrangement is how the next one reaches three paths out of five. There is one `applyDocumentAttrs` now, and the prerender is one of its callerswhich fixes the locale as a side effect of having somewhere to put the zone.
100
+ Behaviour changes that ship with it: `useBlocker` now guards POPSTATE — the Back gesture is a modal's primary close, and it previously bypassed every blocker (the router reverts the moved URL via an entry-index delta and offers retry/reset; ESC in the overlay routes through the same path). `useSearchParams`/`useSetSearchParams` read and write the CALLING TREE's query — a background component can no longer decode the modal's query against its own schema or write onto the modal's URL. Navigation scrolling moved to the visual commit, so an overlay open never scrolls the background. The overlay slot is ALWAYS rendered (null when closed) through the shared provider tree, keeping server/client fiber arity identical (the useId class). The overlay chrome is a native `<dialog>` via `showModal()` platform focus trap, backdrop and focus restoration; body scroll locked while open; deliberately unstyled (`dialog[data-vweb-overlay]`).
92
101
 
93
- ### Internal (no consumer-facing effect)
102
+ Declared non-goal: Next's parallel `@slot` routes — split panes are components in a layout, not a routing concept. Islands/zero-JS pages don't intercept (no client router). Proven by 6 jsdom router tests plus `scripts/intercept-e2e.mjs` on an ssr fixture: standalone SSR HTML with per-photo title and zero hydration warnings, overlay over a mounted background (typed input + mount counter survive open AND close), per-tree search params, nested modals with topmost-only Back, popstate blocking with discard, reload-renders-standalone, and a dev-parity smoke.
103
+
104
+ Measured price: the router group grew 1.5 KB gz (10.5 -> 12.0 KB, the whole first-load delta of this change) -- the overlay stack, popstate blocking and per-tree search params; every other bundle group moved by noise only. The bundle budget is re-pinned to that number.
105
+ - **@voltro/local-first, @voltro/client, @voltro/cli, @voltro/plugin-presence** — The local-first sync engine. A `localFirst()` table's data is now offline readable, editable and convergently resynchronised — through the primitives apps already use, not a second data API.
106
+
107
+ READS: the subscription cache accepts a mirror; every base movement of every subscribed query persists (rows + revision) into a subject+tenant-PARTITIONED IndexedDB store, a cold start seeds `useSubscription` from it (offline reload renders the last materialised rows), and the next connect presents the mirrored revision as `voltro-resume-from` — composing with delta-resume. Deliberately NOT a browser SQL engine: the client's query surface is `(tag, input)`, predicates never exist client-side, so the mirror stores materialised results per query behind a `KvStore` seam (a SQLite backing stays possible without touching a consumer). Soundness is structural: partitioning lives in the KEY (a new subject never finds the predecessor's rows; `purge()` on logout/revocation), `.encrypted()` columns are stripped before every save via codegen-emitted metadata (`voltro dev` writes a zero-import `.framework/localFirst.generated.ts`), a snapshot save REPLACES the row set (revoked/deleted rows evict by construction), and entries gate on a build `schemaFingerprint` — a new build's load is a visible cold start, never a mixed-shape render.
108
+
109
+ WRITES: `useOutbox` gains durability (`persistence:` seam; the one real implementation is `outboxPersistence()` over the same `PersistenceAdapter` the sync queue drains — one durable queue per device) and conflict resolution: `resolveConflict(id, input)` returns a conflicted entry to pending with the resolved input, typically computed by `resolveWithPolicy()` — `crdtText()` columns MERGE, scalars follow the declared `conflictPolicy()`, convergence proven side-symmetric. Multi-tab safety via `withDrainLock` (an exclusive per-partition Web Lock; a host without the API drains unlocked and reports it). Presence rides ONE wire: `usePresenceChannel` in plugin-presence/web adapts the local-first `PresenceChannel` onto the framework's existing presence lane instead of a second transport.
110
+
111
+ Proven end-to-end in a REAL chromium against a real `voltro serve` (`scripts/browser-local-first.mjs`): online seed → 5 offline edits → page RELOAD (queue survives in real IndexedDB, order preserved, mirrored rows render) → drain under the real Web Lock → server and a second browser context converge on all 6 rows; two tabs race the lock and exactly one drains; a foreign subject's binding reads nothing and purge empties exactly one partition.
112
+ - **@voltro/cli** — OG-image generation (plan 13). A page declares its `og:image` as a satori JSX template (`export const ogImage = ({ params, loaderData, locale }) => …`); `static` pages bake the PNG at build time — hashed into `dist/assets/og/`, `og:image`/`twitter:image`/`twitter:card` injected with the absolute `seo.siteUrl`, the page's own `og:image` meta winning over the generated tag — and `ssr` pages serve it on demand over `/_voltro/og`, ONE builder mounted by `voltro dev` AND `voltro start` (head injection lives in the SHARED head builder, so the streamed arm a plain ssr page takes cannot drift from the buffered one — it did, for one commit, and the parity e2e is what caught it).
113
+
114
+ The on-demand URL is signed: HMAC-SHA256 (timing-safe compare) over route + params + tenant + locale — tampering answers 403, tenant/locale ride the signature AND the cache key, and the PNG caches in the same IsrCache backend as the page cache. Secret handling is conditional by design: a single process mints a per-boot secret (sign and verify happen in the same process); a DEPLOY boot with ssr `ogImage` pages and no `VOLTRO_OG_SECRET` refuses loudly — behind a load balancer the signing and the fetching replica differ, and a per-boot secret would 403 every cross-replica fetch. Never a default value.
115
+
116
+ Preconditions are decided, not improvised: a declared `fonts:` family is REQUIRED (the renderer reads the ORIGINAL un-subsetted files; no bundled default font — that would ship a license artifact) with a named error naming the fix; emoji are a declared limit (satori's emoji path is a per-glyph CDN fetch — use an image/data-URI in the template). satori (pinned to an aged release — the workspace's minimumReleaseAge gate is policy) and @resvg/resvg-js ship as script-free optionalDependencies.
117
+
118
+ Proven: renderer core 4/4 (real PNG, size flow-through, distinct-template proof, font refusal), signed route 4/4 (cache HIT, 403, tenant variants render DIFFERENT images, signature-is-not-access), and the `font-pipeline-e2e` extension — build §1b (PNG in dist + absolute tags), dev §2b and `voltro start` §5 (signed URL in the ssr head, PNG, HIT, 403) all green.
119
+ - **@voltro/web, @voltro/cli** — Partial prerendering (plan 20): an isr page that exports `ppr = true` combines a cached, ANONYMOUS shell with per-request dynamic holes on the same response.
120
+
121
+ The design decision, made against React's actual capabilities: a cached stream prefix cannot be RESUMED in stable React (postponed state is experimental), so ppr is client composition on the existing `defer()` seam. The shell is the normal buffered isr render — eager fields in the HTML, each hole as its `<Await>` fallback, cached as a plain IsrCacheEntry (no cache shape change, same revalidate/CDC/on-demand invalidation). On every serve (hit, stale, miss) the response stays open after the shell bytes: the page loader runs again with the FULL request context and each deferred field is appended as a registry settle script the moment it resolves. The client reveals holes through hydration.
122
+
123
+ The shell render is fail-closed, not merely stripped: its loader context and `useServerRequest()` snapshot THROW by name on credential access (cookie, authorization, x-voltro-*; any cookie but voltro:locale) — the first request answers with an error naming the read and the fix ("move it into a deferred hole"), instead of baking silently-empty subject data into an artefact served to everyone. Holes are async functions; their credential reads happen inside the promise and see the real request only on the hole pass.
124
+
125
+ Static + ppr stays refused (a static file host cannot append anything — isr with a long revalidate is that page); ppr requires `interactive: 'full'`; layout loaders cannot defer on a ppr page (v1); csp nonces are refused as on isr. `voltro dev` mirrors the whole behaviour through the shared `pprRender.ts`. Proven end-to-end by `scripts/ppr-e2e.mjs` with a FILE-GATED hole (deterministic, no timing waits): shell chunk received while the hole is provably open, settle script after the gate opens, per-subject hole content with a byte-identical subject-free shell prefix across subjects, cache HIT on the second request, the named 500 for an eager credential read, dev parity, and a browser client-navigation rendering the hole via the client defer path.
126
+ - **@voltro/plugin-presence** — `presencePlugin({ resolveMember })` — resolve the fields other channel members see about a caller (display name, avatar URL) server-side, from the authenticated subject. `meta` is client-supplied and handed to every channel member verbatim, which is the right contract for a cursor and the wrong one for identity: any member could present any name and any `<img src>` to everyone else. The resolver runs on every heartbeat and its result merges OVER the caller's `meta`, so a client cannot override what the server says about them; returning `undefined` declines and leaves `meta` untouched. The docs and the package description now say plainly that `meta` is unvalidated and relayed verbatim — identity does not belong in it.
127
+ - **@voltro/plugin-queue, @voltro/cli, @voltro/devtools-ui, @voltro/plugin-cdc-out, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-mssql, @voltro/sql-sqlite** — `@voltro/plugin-queue` — interop with a Kafka an adopter already runs, as the door to foreign queues (the outbox stays the path for your OWN durable side-effects, workflows for your own orchestration). Kafka first; the `QueueProvider` contract is cut so SQS/RabbitMQ can be later implementations.
128
+
129
+ Consuming is a file convention: `*.consumer.ts` exports a `defineQueueConsumer({ topic, schema, handler })`, discovered on BOTH boot paths and started at plugin activation. The semantics are deliberate and documented: at-least-once with per-message commit (a process killed mid-batch redelivers exactly the unhandled tail), serial per partition (parallelism only ACROSS partitions; retry backoff blocks the partition on purpose), decode failures dead-letter immediately to `<topic>.dlq` with `x-voltro-dlq-*` reason headers (a deterministic failure retried forever is an infinite loop with extra steps), handler failures retry with backoff then dead-letter after `maxAttempts`, and a rebalance is never counted as a failure. Handlers get `ctx.store` but are NOT transaction-wrapped (the HTTP-handler boundary) and must be idempotent. Replica coordination is Kafka's own consumer group — no advisory lock, unlike schedules, which have no broker to do it for them.
130
+
131
+ Producing: transactional-with-a-write goes through the existing outbox (`ctx.outbox.enqueue('queue.produce', …)` + a `queueOutboxHandler()` bridge file — one durability path, not a second), fire-and-forget through `QueueService.produce`. Topic creation is EXPLICIT (`ensureTopics`) — whether a client may create topics on foreign infrastructure is the adopter's policy; a consumer on a not-yet-existing topic warns and retries in the background, never aborting the boot. `kafkaSink` plugs cdc-out table mirroring into the same provider (key = row id, `x-voltro-delivery-key` dedupe header).
132
+
133
+ Observability: `GET /_voltro/inspect/plugins/queue/consumers` + a Queue panel in both dashboards; `traceparent` flows from message headers to `ctx.traceparent`.
134
+
135
+ Found by the e2e (mutation → outbox → real broker → second process's consumer → row; kill a fleet member mid-flow → the survivor takes over with no duplicate row): the INSERT branch of `upsert` never stamped a generated `id`, so an upsert into an auto-id table failed on the NOT NULL constraint — in all four dialect stores. Fixed in all four (`stampGeneratedId` at the top of `executeUpsert`, as `insert` already did), pinned by a source-parity test; the default DO-UPDATE column set already excludes `id`, so a conflicting row keeps its identity.
136
+ - **@voltro/client, @voltro/ui, @voltro/testing** — Three more pieces of the form contract:
137
+
138
+ - **Reference fields.** `formField({ reference: 'stores' })` marks a schema field as a table reference — `widget: 'reference'` carrying its target table, the value stays the id (or id list, feeding a target's declared `relations:`). The default registry renders the render-prop note (a live picker needs a query binding only the app can name); `WidgetKind` grew accordingly (covered by the 0.53.0/04 note). - **`formSections(fields)`** groups an ordered field list into contiguous sections, and `<FormSkeleton>` renders them — title rows included, so the placeholder has the SHAPE of the real form. - **`renderFormBinding`** in `@voltro/testing/client` drives the REAL binding against the fake api: fill / blur / submit / visible errors / state, with server field errors injected by simply throwing `ValidationError({ field })` from the mutation handler — the same routing path a production refusal takes. Needs jsdom; react-dom loads lazily so the non-form harness stays React-DOM-free.
139
+ - **@voltro/web, @voltro/cli** — `<Script>` — third-party scripts with a declared loading strategy (plan 23). `afterInteractive` (default, injected after hydration, never render-blocking) and `lazyOnload` (browser idle via requestIdleCallback with the setTimeout fallback). Inline variant with a REQUIRED `id` as its dedupe key. Deliberately absent: `beforeInteractive` (the honest answer for a must-run-first script is a literal tag in the shell head — a preload link fetches but never executes) and `worker` (Partytown-class, its own decision).
140
+
141
+ Dedupe rides a PROCESS-GLOBAL registry (globalThis + Symbol.for — not React context, not module scope: islands entries are separate bundles and every island is its own hydrateRoot). A script is never unloaded; a re-mount of the same src/id injects nothing and re-fetches nothing, but `onLoad` fires again from the registry cache — the next/script remount bug class, pinned by e2e. Cache callbacks are cancelable microtasks so StrictMode's dev double-effect cannot double-fire a visible mount's onLoad.
142
+
143
+ Behavior per `interactive` mode is DECIDED: on `'none'` the bundle never ships so a `<Script>` can never fire — the build warns by name; on `'islands'` the page's static part never mounts — the build warns and the answer is moving the script into an `*.island.tsx` (it then loads when that island hydrates). CSP: an explicit `nonce` prop wins; otherwise the injector propagates the document's own nonce (SSR pages under the middleware's `cspNonce` get it automatically); static pages have no per-request nonce path — `'strict-dynamic'` or a hash policy is the documented answer.
144
+
145
+ Proven: 7 jsdom unit tests (dedupe, cached callbacks, id refusal, stubbed rIC + Safari fallback, nonce propagation) + a real-chromium e2e (`scripts/browser-script-component.mjs`, 11 checks: hydration-before-script ordering without sleeps, one request for two tags, remount onLoad without a second request, both build warnings asserted against a real `voltro build`).
146
+ - **@voltro/protocol, @voltro/runtime, @voltro/client** — Server-side FIELD validation, end to end. `@voltro/protocol` gains the browser-safe `ValidationError({ field, message, params? })` and `ValidationErrors({ issues })` — the constructors the docs promised for several versions while no package exported them — auto-merged into every mutation's and action's wire error union (exactly like `ScopeError` and `BusinessRuleViolation`), so no descriptor ever declares them. Executors raise them through the new, always-present `ctx.validation`:
147
+
148
+ ```ts
149
+ if (await emailTaken(input.email)) {
150
+ return yield* ctx.validation.fail('email', 'validation.emailTaken')
151
+ }
152
+ yield* ctx.validation.require(input.startsAt < input.endsAt, 'endsAt', 'validation.beforeStart')
153
+ ```
154
+
155
+ `useFormBinding` now ROUTES them: the error lands in `errors[field]` (translated through the message catalog), the form stays editable, and `submitError` only ever carries what no field can — a banner listening there stops double-reporting every field refusal. A `BusinessRuleViolation` whose rule pinpointed a `field` routes through the same path; the one reader for all three shapes is `fieldIssuesOf` in `@voltro/protocol`, so a custom widget kit cannot disagree with `<AutoForm>` about which errors belong on a field. Store-opaque failures (unique violations without a rule) still surface as `submitError` — mapping them to a column is the declared next step, not silently half-done.
156
+ - **@voltro/client** — `useFormBinding` closes two gaps between the docs' promise and the hook. `asyncFields` puts `useAsyncValidation` INTO the submit path: in-flight checks are awaited (bounded, default 5s, fail-closed to `validation.checking`), an `invalid` verdict blocks the submit with the message on ITS field — the uniqueness probe no longer runs beside the form while `submit` ignores it. And `createHooks` now types the binding: `useFormBinding('employees.update', …)` takes the tag as a literal, `values`/`defaults` and the submit output infer from the generated descriptor — same treatment `useSubscription`/`useMutation`/`useAction` already had.
157
+ - **@voltro/runtime, @voltro/cli, @voltro/voltro** — Subscription socket backpressure (plan 01 phase 1). Measured first: a consumer that stopped reading retained EVERY event — 300 changes, 300 retained events, the producer never slowed — so one dead dashboard tab grew the process without bound.
94
158
 
95
- - **@voltro/cli** `voltro data backup --assets` / `restore --assets` are now driven against a REAL S3 API (MinIO in the test stack), over the network, with a real backup and a real restore into a second bucket and the bytes compared.
159
+ Now a per-subscription outbox sits between the dispatcher's synchronous emit and the socket stream. A pump fiber awaits each event's acceptance; while the consumer is blocked, updates COALESCE onto the newest state and the next accepted event is ONE patch computed against the state of the last event actually handed over — patch continuity holds across any number of collapsed intermediates (proven by materializing the received events client-style and comparing against the final server state). Revisions jump forward under coalescing; wire-protocol.md documents the jump as normal, never a gap. Memory per blocked subscription is bounded by construction: one pending state, however far behind the consumer is.
96
160
 
97
- The asset half rests on one field: a provider must map "there is no object at that key" to `status === 404`, and nothing looser a 403 from a rotated credential is also non-transient, and calling that "the object is gone" turns a recoverable outage into a backup that quietly contains nothing. That mapping was measured against memory, filesystem and database live, and against the s3/azure SDK error SHAPES constructed. A constructed shape is a claim about an SDK, not about a round trip: nothing in it exercises signing, path-style addressing, or what the SDK actually raises when a server answers `NoSuchKey`. Two deployments listed exactly this as the gap they could not close either.
161
+ The terminal policy is loud: a consumer persistently over `reactive.socket.maxBufferedBytes` (default 1 MiB) for `reactive.socket.overrunAfterMs` (default 10 s) receives a typed `SubscriptionOverrun` error event carrying `bufferedBytes` and `maxBufferedBytes` and the stream ends; the client re-subscribes for a fresh snapshot. Never a silent drop. Oversized events are telemetry, not a cap: over `reactive.socket.oversizedEventBytes` (default 256 KiB) the event is delivered normally, counted and WARN-logged with the query tag.
98
162
 
99
- Both directions are covered against the real server: a dangling reference is stepped over and reported, and a bad credential fails the capture rather than being read as a missing object.
163
+ All three knobs live in `app.config.ts` under `reactive.socket` with env overrides (`VOLTRO_REACTIVE_MAX_BUFFERED_BYTES` / `VOLTRO_REACTIVE_OVERRUN_AFTER_MS` / `VOLTRO_REACTIVE_OVERSIZED_EVENT_BYTES`), resolved by ONE resolver both boot paths wire. Four metrics ride the shared snapshot the Prometheus exporter and the inspect Metrics panel read: `voltro_subscription_buffered_bytes`, `voltro_subscription_coalesced_total`, `voltro_subscription_overrun_total`, `voltro_subscription_oversized_total`.
100
164
 
101
- The native dialect lane also stops being silent about mssql. It was absent from the array entirely an absent lane and a covered one look identical from the outside and it is now listed with a written reason for why it does not register here (`sqlpackage` is a separate Microsoft download on a .NET runtime, absent from `mcr.microsoft.com/mssql-tools`). It is registered rather than skipped-forever, because a skip present on every healthy run teaches readers to ignore skip lines; and an assertion fails if any lane drops out WITHOUT a written reason, or if a reason names a lane that is in fact running.
102
- - **@voltro/cli, @voltro/plugin-storage** — Coverage for the data commands, at the level the defects actually live.
165
+ `apiSurface: compatible`additive: the outbox/tunables/metrics exports on `@voltro/runtime`, an optional `socket` block on `ReactiveConfigInput`, and an optional trailing parameter on `bindSubscriptionUntyped`.
166
+ - **@voltro/plugin-notifications, @voltro/cli, @voltro/env, @voltro/protocol, @voltro/devtools-ui** — Web Push (VAPID) as a notification channel — `webPushChannel()` in `@voltro/plugin-notifications`: a browser subscribes once (`useWebPush()` + the shipped `sw.js`) and receives notifications with the tab closed.
103
167
 
104
- `dataDirectFlags.e2e.test.ts` drives every DIRECT-target flag of `voltro data export` / `import` through the real binary against a real sqlite database, and carries the same `DATA_FLAGS`-driven self-check the native suite has: a new direct flag has to be driven there or the file goes red. The api-only flags are listed explicitly with the reason they are not here, and that list is asserted against `DATA_FLAGS` so it cannot become a place to hide an untested flag.
168
+ The protocol layer is an own ~250-line implementation over `node:crypto` (RFC 8291 aes128gcm encryption + RFC 8292 VAPID ES256), pinned byte-for-byte against RFC 8291 Appendix A — no dependency tree for what is one HKDF chain, one AES-GCM call and one JWT. ONE secret, `VOLTRO_VAPID_PRIVATE_KEY` (a base64url P-256 scalar): the browser-facing public key is DERIVED from it, so a public/private pair can never desync. `voltro dev` mints it per project into the gitignored `.env.local` (the new `p256` mint encoding, and plugins can now declare their own mintables via `PluginEnvVar.generate`); a production boot with the channel configured and no key refuses by name.
105
169
 
106
- `missingObjectIs404.test.ts` pins the contract the dangling-reference skip rests on: every provider maps "no object at that key" to `status === 404`, and nothing looser. Five providers, three of them live, s3 and azure through their SDK's real error shapes which read DIFFERENT fields (`$metadata.httpStatusCode` vs a bare `statusCode`), so a mapping copied from one to the other would turn dangling references back into hard capture failures on that backend alone.
170
+ Subscriptions live per subject AND per endpoint (`_voltro_notification_push_subscriptions`, unique on a sha256 endpoint hash — endpoint URLs can exceed the unique-index byte ceiling on mssql/mysql). Delivery is per endpoint and ISOLATED, and this fix reached the existing mobile `pushChannel` too: its old loop threw at the FIRST rejected token, aborting every remaining device's send and collapsing the outcome into one per-channel `failed` row. Both channels now implement `deliverDetailed`; the delivery log records one row PER ENDPOINT (`endpoint` column), the channel counts delivered when at least one endpoint was reached, and `pushChannel` gained `onTokenRejected(token, reason)` as the app-side prune hook. Web push prunes itself: a push service answering 404/410 deletes exactly that endpoint's row — the subject's other browsers keep receiving.
171
+
172
+ Also in the box: payload cap handling (over ~4 KB the payload SHRINKS — `data` first, then the body truncates — never dropped), click tracking (a per-delivery token rides the payload; the service worker's `notificationclick` reports it and the record gains `clickedAt` — the token itself never reaches a dashboard reader), quiet hours / digests / preferences applying unchanged (preference key `webPush`), subject-bound subscribe/unsubscribe RPC mutations, and the dashboards' delivery panel showing per-endpoint rows + a clicked badge.
173
+
174
+ Proven twice, per the plan's split: a mock-push-endpoint suite asserts the VAPID JWT verifies against the derived public key, the body decrypts with the subscriber's keys, TTL rides the request, the oversize payload shrinks, and the 2-endpoints-1-dead case delivers one and prunes one; a real-chromium e2e (`scripts/webpush-e2e.mjs`) registers the SHIPPED service worker, delivers a simulated push over CDP, and asserts the event fires with the payload intact — and not at all after unregistering.
175
+
176
+ ### Changed
177
+
178
+ - **@voltro/runtime** — Matcher authority for subscription wakes: a subscription whose query is a plain predicate read is now woken by the predicate index ALONE. The IndexedMatcher has always routed change events precisely (column, range and composite-tuple buckets, parity-pinned against a linear scan) — and its selectivity was then discarded, because every subscription was also registered as a dependent of its own table and the dispatcher unioned the two sets. Measured before: 200 of 200 subscribers whose predicate matched NOTHING were woken (and re-queried) by one write on their table. After: 0.
179
+
180
+ The soundness argument for authority: any change that can move a pure descriptor read's result involves a row whose OLD or NEW image matches the predicate — ordered limit/offset windows included, since a row shifting the window matches it itself. A per-delivery row filter does not break it either: the filter only ever ANDs onto the base predicate the matcher indexes, so a base-predicate wake is conservative (pinned by its own test). Everything the matcher cannot soundly judge keeps the conservative table-wide wake: queries with an eager spec, a setOp or a CTE (a self-referential eager collapses to "own table only" while reading rows the root predicate does not describe — the classification is structural, not set-based), `dependsOn` raw reads, computed and `reactivityChannel` queries, and OVERSIZED change events (`tombstone`/`unrecovered` images the matcher cannot see wake the whole table for that one event; `rehydrated` is judged normally — previously the `oversized` marker was read by nothing, hidden behind the same wildcard).
181
+
182
+ Measured consequence (fanout-ceiling.mjs, three runs): the distinct per-user shape (`where userId = me`) now pays a bucket lookup plus ONE delivery per write regardless of resident subscriber count — flat, no longer ~22–29 µs per subscriber per write — so a selective `where` buys real headroom, and the ceiling is set by the shared all-match shape (0.37–0.41 µs/subscriber, ≈25–27k subscribers per node at 10 writes/s against 10% of one core). The ceiling script now FAILS if a never-matching subscriber is woken, so the wildcard cannot quietly return. Also new: a chaos test pinning that one stalled consumer among 500 healthy ones neither delays the healthy population nor grows the server (its backlog coalesces onto the newest state, bounded).
183
+
184
+ ### Fixed
185
+
186
+ - **@voltro/data-transfer, @voltro/runtime** — The five framework tables the last two plugins added are classified for transfer.
187
+
188
+ `voltro data export --scope all` refuses to run until every framework table is either portable or environment-local, and the four comment tables plus web push's subscription table were neither. They are now:
189
+
190
+ - the comments, threads, reactions and read-markers are **portable** — they are what the app's users wrote, and moving them is the reason a transfer exists. - `_voltro_notification_push_subscriptions` is **environment-local**. A browser push endpoint is bound to the deployment that minted it: the `applicationServerKey` the browser subscribed with is derived from `VOLTRO_VAPID_PRIVATE_KEY`, so a push service refuses a delivery signed by any other one. Importing a foreign row makes the target attempt a delivery that must fail — and the auto-prune then deletes a subscription that was valid where it came from.
191
+
192
+ Also: one composite map key carried a raw NUL character instead of the `\u0000` escape, which made its file BINARY to every text tool. The runtime value is identical; what changes is that `grep` can read the file again.
193
+ - **@voltro/runtime, @voltro/workflow, @voltro/plugin-ratelimit, @voltro/cli** — Two more ways a production stream got bare text between its JSON records, both now closed:
194
+
195
+ - **`Effect.log*` rendered through Effect's DEFAULT logger** — a multi-line `timestamp=… level=WARN fiber=#…` logfmt block — anywhere the framework runs an Effect runtime without the framework logger installed: the workflow/cluster engine, the plugin SQL runtime, the kv facade, the rpc server's own fibers, and every detached handler fiber (`Effect.runFork` starts from the default runtime, so a handler's `Effect.logInfo` never saw the server's logger). Every production runtime now carries `LoggerLayer` — once per runtime root, because a second replace in one fiber stack prints every line twice — so `Effect.log*` in user handlers, workflow executors and cluster internals renders as the framework's JSON in a pod and pretty on a TTY. - **Bare `console.*` in server-side packages**: the row-filter load failure (request and subscription paths) and the rate-limit shield's degrade-to-unlimited warning now log through scoped framework loggers. `awaitSignal`'s suspend hint routes through `Effect.logWarning` inside the workflow runtime instead of a console fallback.
196
+
197
+ The production-stream source guard now also pins both classes: no `console.*` in the server packages it watches, and no `ManagedRuntime.make` in a production command without `LoggerLayer`.
198
+ - **@voltro/cli** — `voltro serve`'s `/_voltro/inspect/data/tables` now serves the MERGED table set (app entities + framework-assembled tables + analytics), as `voltro dev` always has. It served the app's entities alone, so `voltro check --url` — which reads that endpoint as its idea of which tables exist — reported a query whose `source:` names a framework table (`_voltro_agent_messages`, say) as a dangling-source ERROR against a live server, exit 1, while `--offline` said OK about the same declaration: the offline manifest assembles the framework tables itself, and the two modes contradicted each other. The full set was already computed a few lines above (the stale-`source:` audit refuses to run without it, for this exact reason) — the inspect surface just never received it. The masked data browser gains the same tables.
199
+ - **@voltro/client** — Auto-optimistic `op: 'update'` now merges into a SINGLE-OBJECT cache entry — a `*.getById` read — exactly as it merges a list row, and `op: 'delete'` empties one to `null`. The reducer's list handling fell through `Array.isArray(current) ? current : []` for a single object, so a two-field PATCH replaced the whole row until the server snapshot arrived: for ~400ms a detail page rendered only the patched fields — no assignee, a `createdAt` of "Invalid Date", nothing the input did not carry. The doc sentence "update merges by id" now holds for both shapes it covers; a patch whose id does not match the cached object leaves it untouched, and the same rule applies to a single object at a nested target path.
200
+ - **@voltro/runtime, @voltro/cli, @voltro/voltro** — Two optional-parameter declarations that were not optional enough.
201
+
202
+ **`MutationLike.descriptor.target.relations`** was declared `relations?: Readonly<Record<string, string>>` while every sibling field in that interface carries `| undefined`. With `exactOptionalPropertyTypes` on, `relations?: X` REFUSES an explicit `undefined` — and `InsertTarget` types it exactly that way, so a concrete `DiscoveredMutation` stopped being assignable to `MutationLike`. One missing union member, 153 compile errors across `dev.ts` and `serveApi.ts`, and the CLI's whole boot path did not typecheck.
203
+
204
+ **`wrapCaptureStore`'s CRDT resolver** now defaults, like the one on `makeUndoCapture` that passes straight into it. Required there and optional here was the same information decided two ways inside one change, and the required form turned a two-argument call that compiled into one that does not — for a `@public` export the umbrella re-exports as `voltro/server`. Omitted means "no CRDT columns", which is what a caller written before the feature meant. The deliberate parity guard is unaffected: `undoCaptureDep` still REQUIRES its resolver, by design and with the reason written at the declaration.
205
+
206
+ Also here: `voltro serve` read a bare `schemaRegistry` at two sites where the registry lives on `opts` — the dev/serve copy, loud this time because it does not compile.
207
+ - **@voltro/plugin-presence** — `usePresence` / `useTyping` no longer fire their join/heartbeat into the client-boot window of an SSR page. The mount effect called `mutate` against the not-yet-resolved api; the stub throws, the error escaped the effect as an uncaught pageerror (no boundary catches an effect), and the first join was simply lost — the member appeared only at the next interval beat. The hooks' own roster subscription is the resolution signal (it stays idle until the client is real), so the join is sent on the first snapshot — an empty roster counts — and the unmount leave is gated the same way.
208
+ - **@voltro/logger, @voltro/cli** — In a `json`-format log stream (the default off a TTY, so every pod), EVERY line the framework emits is now a parseable record. Previously a production tail mixed JSON records with bare text from three sources: five subsystems whose serve-path wiring handed them hand-rolled `process.stdout.write('[tag] …')` adapters instead of the logger (schedule, broadcast, workflow ×2, flow-control — dev handed the real logger through, so every dev terminal looked right); the boot banner and app surface, which stripped their colours off a TTY and printed the multi-line layout anyway; and `voltro db apply`'s plan summary and refusal detail, rendered as terminal tables into a migrate job's stream.
209
+
210
+ `@voltro/logger` now exports `logFormat()` — the same pretty/json resolution the loggers use — and every report renderer consults it: on a TTY the banners and tables render exactly as before; in `json` mode each becomes one structured record carrying the same numbers as fields (the plan summary includes the per-op lines, the fingerprints and the rolling-deploy advisory). Refusals on the apply path carry their detail and fix as fields of the SAME record as their headline instead of raw lines under it. A source guard pins the `[tag]`-adapter idiom out of the production boot-path files so a sixth subsystem cannot reintroduce it.
211
+ - **@voltro/cli** — **An unmatched path is a server-rendered 404 now, in both boot paths.** The best-matching `not-found.tsx` (deepest owning directory, group segments excluded — the same pick the client router makes) renders through the ssr arm with status 404 and `x-voltro-rendered-by: ssr-not-found`; an app without one gets a plain-text 404. Previously `voltro dev` served a 200 client shell for any unknown path — so crawlers indexed error pages, link checkers needed a browser to see failures, monitoring read "fine", and the user saw a shell and then the client-side not-found jump — and `voltro start` answered a bare text 404 without the app's page. One shared predicate (`bestNotFoundDir`), so dev and start cannot disagree about which file answers a miss.
212
+
213
+ **`voltro probe access` can judge procedures with required input.** It sent `{}` for every probe, so any guarded procedure whose input has required fields died in the decoder before the guard ran and probed `inconclusive` — measured at 704 of 914 on one deployment, 77% of the surface unjudgeable by the tool that exists to judge it. The probe now synthesizes a minimal payload from the SAME input schema the server enforces (read from the local checkout's descriptors; required scalars/enums/literals filled, optionals omitted). The old reasoning survives in the failure direction: when synthesis is impossible or wrong, the call dies in the decoder exactly as `{}` did and the verdict degrades to `inconclusive` — never to a false refused/admitted.
214
+
215
+ Also pinned: a NEW column and its FOREIGN KEY in the same plan land in ONE `db apply` (integration-tested against a real postgres — plan carries add-column + add-foreign-key + add-index, the re-plan is empty, the constraint is live). Reported long ago against an early planner and never re-measured; the round-trip test keeps the ordering from regressing into needing a second run.
216
+ - **@voltro/cli** — `voltro update` can cross a package rename. The pin sweep bumped the OLD package name to the target version — a version never published under that name — so the install failed with `NO_MATCHING_VERSION`, and the codemod that performs exactly this rename sat inside the target version the failed install never put on disk. "Fix the install" was the rename; the user did the circle by hand.
217
+
218
+ Renames are now data (`packageRenames.ts`), and they ride the same published manifest the codemod preview already reads (`voltro.renames` beside `voltro.codemods`), so the OLD cli running the update learns the target's renames through the registry query it already makes — before anything is installed. The sweep then moves the dependency KEY and bumps the version in one write; a `workspace:`-pinned dep stays skipped rather than half-renamed. When the manifest cannot be fetched, the running cli's own rename registry is the fallback, and the install-failure message now names the rename circle so a user who still hits it knows the three manual steps.
219
+ - **@voltro/cli** — `restRoutes` declared on a `type:'web'` app now REFUSES the boot (`voltro dev` and `voltro start`, one shared predicate) instead of being silently ignored. The silent form was the worst available behaviour: a readiness probe hitting a declared `/api/health` got the SPA shell with a 200 — green probe, handler never reached — a POST got 404, and call sites ran against a dead same-origin path with nothing anywhere saying the config was inert. The refusal names the fix: REST routes mount on the API process; same-origin paths belong to the ingress/proxy.
220
+
221
+ Also: `voltro secret generate inspect-write` mints the mutating-inspect second factor (`VOLTRO_INSPECT_WRITE_TOKEN`), and the 401 that demands it now names that command — the message named the header and the variable and left "where does the value come from" to guesswork. And a re-issued codemod note (`0.53.0/02`) reaches everyone who crossed 0.37.0 with `input: Schema.Struct({})` procedures: that shape flipped from accept-everything to reject-everything, which the original note never named — and a published note cannot be amended for anyone already past it.
222
+
223
+ ### Internal (no consumer-facing effect)
107
224
 
108
- `codegenFeatureTables.integration.test.ts` measures the count a report was about: `voltro codegen` must carry the tables a `*.cron.tsx` contributes, which the entity walk cannot see. The structural guards beside it were TRUE while that count was wrong. `frameworkSourceTypo.integration.test.ts` measures what catches a misspelled `_voltro_*` source given that the type deliberately does not `voltro check` reports it as a dangling source and exits 1, with a negative control so the check is not merely flagging every framework name.
225
+ - **@voltro/cli, @voltro/web** Release-gate findings, all in guards or infrastructure — no runtime behavior changes: the OG-route signing separator is written as the `\u0000` escape instead of a literal NUL byte (a NUL makes the file binary to grep, so every text-based audit silently skipped it); the cookie-jar boot-path guard follows the ppr shell branch (`pprShellCookies` over the shared-render allowlist); the native-leaf rationale recognises prebuilt-platform-package natives (`sharp`/`@resvg/resvg-js` carry their `.node` in platform-triple optionalDependencies) and records `fontkit`/`subset-font`/`satori` as documented dynamic-import leaves (optional at build time, interop proven by the font/OG e2e); the `voltro data` e2e suites give their spawned CLIs an isolated HOME so `guardLive`'s machine-registry fan-out cannot see an unrelated live `voltro dev` the operator runs; and the CI/gate test stack starts `kafka-test` (with a broker-answering healthcheck), so the plugin-queue integration suite runs non-vacuously instead of loudly skipping.
@@ -12,16 +12,15 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.97.0",
14
14
  "@effect/rpc": "^0.76.0",
15
- "@voltro/ai": "0.51.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/database": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/protocol": "0.51.0",
20
- "@voltro/runtime": "0.51.0",
15
+ "@voltro/ai": "0.53.0",
16
+ "@voltro/cli": "0.53.0",
17
+ "@voltro/database": "0.53.0",
18
+ "@voltro/env": "0.53.0",
19
+ "@voltro/protocol": "0.53.0",
20
+ "@voltro/runtime": "0.53.0",
21
21
  "effect": "^3.22.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.51.0",
25
24
  "typescript": "^6.0.3",
26
25
  "@vitest/coverage-v8": "^4.1.10",
27
26
  "vitest": "^4.1.10"
@@ -1,4 +1,4 @@
1
- // Unit test with `@voltro/testing`. `support.summarize` is descriptor-pinned:
1
+ // Descriptor-pin unit test, plain vitest + effect. `support.summarize`:
2
2
  // its executor calls `generateObject` (a real model inference), which a unit
3
3
  // harness can't provide — so we do NOT run the executor. Instead we assert the
4
4
  // wire CONTRACT that both the client and the model are bound to: the action's
@@ -13,17 +13,17 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/database": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/plugin-auth": "0.51.0",
20
- "@voltro/protocol": "0.51.0",
21
- "@voltro/runtime": "0.51.0",
22
- "@voltro/sql-postgres": "0.51.0",
16
+ "@voltro/cli": "0.53.0",
17
+ "@voltro/database": "0.53.0",
18
+ "@voltro/env": "0.53.0",
19
+ "@voltro/plugin-auth": "0.53.0",
20
+ "@voltro/protocol": "0.53.0",
21
+ "@voltro/runtime": "0.53.0",
22
+ "@voltro/sql-postgres": "0.53.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.51.0",
26
+ "@voltro/testing": "0.53.0",
27
27
  "typescript": "^6.0.3",
28
28
  "@vitest/coverage-v8": "^4.1.10",
29
29
  "vitest": "^4.1.10"