@voltro/cli 0.52.0 → 0.54.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 (244) hide show
  1. package/CHANGELOG.md +424 -0
  2. package/THIRD-PARTY-NOTICES.md +8311 -3318
  3. package/dist/{agentsMd-0l980yhL.js → agentsMd-BaLC10Na.js} +110 -82
  4. package/dist/agentsMd-DCY1RSs8.js +2 -0
  5. package/dist/apiBuild-CeUN55uk.js +2 -0
  6. package/dist/{apiBuild-CSFI8QGq.js → apiBuild-DTWp0S_q.js} +11 -5
  7. package/dist/bin.js +1 -1
  8. package/dist/build-D4ygSbnV.js +843 -0
  9. package/dist/{checkCommand-COmqc2cB.js → checkCommand-Dg1G7Gwd.js} +6 -6
  10. package/dist/{checkCommand-2SbqzukH.js → checkCommand-L7DTlpIF.js} +1 -1
  11. package/dist/{cloudCmd-QUXh-b5w.js → cloudCmd-DzKcSYuy.js} +1 -1
  12. package/dist/{codegen-VF479Cnb.js → codegen-DSLM8Su9.js} +1 -1
  13. package/dist/codegen-DjgxEOnD.js +2 -0
  14. package/dist/codegenCommand-CG_Vx4lc.js +41 -0
  15. package/dist/{codemodRunner-r7J9lIa7.js → codemodRunner-Cd4xkC6u.js} +109 -11
  16. package/dist/{commands-Cc_nV8WI.js → commands-6Kzi92Np.js} +96 -73
  17. package/dist/{dashboardCommand-C-gKvwqh.js → dashboardCommand-Cq1PWvI1.js} +5 -5
  18. package/dist/{dataCommand-BgpBHnlB.js → dataCommand-DYzW8vkv.js} +3 -3
  19. package/dist/{dbCommand-sHedr-NJ.js → dbCommand-B4NWZtGL.js} +278 -237
  20. package/dist/dbCommand-CSFWs9ev.js +2 -0
  21. package/dist/{dev-CRHoCEiy.js → dev-CmuvUKRq.js} +2903 -2306
  22. package/dist/{dev--A3nsxA3.js → dev-cKUiZZsB.js} +1 -1
  23. package/dist/{doctorCommand-DtfJ3FA6.js → doctorCommand-DCiFVMtZ.js} +101 -69
  24. package/dist/doctorCommand-J3qu4E0Y.js +2 -0
  25. package/dist/{dormancyCommand-Drn7o0No.js → dormancyCommand-w1TrmgYP.js} +1 -1
  26. package/dist/{embeddingsCommand-Z-jO1fWN.js → embeddingsCommand-CMgPyRTr.js} +1 -1
  27. package/dist/{envCommand-D4gCrrTZ.js → envCommand-Cyynmcfa.js} +8 -8
  28. package/dist/{evolveCommand-CMROeKeA.js → evolveCommand-BwvQ8dVH.js} +2 -2
  29. package/dist/fileConventions-l-RIXbx8.js +36 -0
  30. package/dist/{fileTaxonomy-DvDUV9wq.js → fileTaxonomy-CbyMQYx_.js} +42 -42
  31. package/dist/fontPipeline-LxIHa1vo.js +2 -0
  32. package/dist/fontPipeline-Tsh8kZfA.js +152 -0
  33. package/dist/{frameworkTableAssembly-w-XnLa3q.js → frameworkTableAssembly-D7LJuALW.js} +5 -5
  34. package/dist/frameworkTableAssembly-IPD1pUnZ.js +2 -0
  35. package/dist/imagePipeline-B_GVJgm6.js +2 -0
  36. package/dist/imagePipeline-CBZmjT4i.js +127 -0
  37. package/dist/index.js +2 -2
  38. package/dist/{infoCommand-DXM868o_.js → infoCommand-DlYlUPqs.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/manifestBuild-C4-J1-m_.js +2 -0
  43. package/dist/{manifestBuild-CuU1VrSm.js → manifestBuild-Cqgsx2bM.js} +1 -1
  44. package/dist/{metaCommands-C6RFmF1r.js → metaCommands-x7RCi2AF.js} +9 -3
  45. package/dist/{migrate-D0F-eTlK.js → migrate-BK_Bbx-_.js} +2 -2
  46. package/dist/mobileCommand-D9O6iq3D.js +428 -0
  47. package/dist/mobileCommand-DAum7tsG.js +2 -0
  48. package/dist/{pageConvention-CzUiSbtU.js → pageConvention-CMpfDN6r.js} +1 -1
  49. package/dist/{privacyCommand-DGdopOI6.js → privacyCommand-BCa2OoZG.js} +1 -1
  50. package/dist/{probeCommand-C9gazU0H.js → probeCommand-_C0YU207.js} +83 -24
  51. package/dist/{projectScaffold-EzlErR4E.js → projectScaffold-CJfP-xbT.js} +1 -1
  52. package/dist/{projectScaffold-B4dmTlwT.js → projectScaffold-CSN0OzBV.js} +2 -2
  53. package/dist/renderModeScan-43yQ2opo.js +147 -0
  54. package/dist/{renderProfile-Ck32Fzxr.js → renderProfile-DvrhVJHa.js} +2 -2
  55. package/dist/{runtimeTrace-BPQyCmC5.js → runtimeTrace-CGWx1Q6l.js} +1 -1
  56. package/dist/{sdkgen-Se88ifTd.js → sdkgen-CDGHQUFj.js} +1 -1
  57. package/dist/serveCommand-BiPe8BJm.js +2 -0
  58. package/dist/{serveCommand-DkP3OT0W.js → serveCommand-Bje09q1v.js} +889 -825
  59. package/dist/serveEntry.js +1 -1
  60. package/dist/start-B0bnJgxI.js +3 -0
  61. package/dist/{start-jw89Xbqy.js → start-Clz-1BHB.js} +633 -455
  62. package/dist/startEntry.js +1 -1
  63. package/dist/{staticCommand-BwNEDlSU.js → staticCommand-ey0kYmOT.js} +1 -1
  64. package/dist/{subcommandNames-DpYs3DXr.js → subcommandNames-CDzfEtKV.js} +3 -3
  65. package/dist/{templates-BR-fb4SP.js → templates-BTWZkJJT.js} +41 -9
  66. package/dist/{test-f3amja6a.js → test-D_kW4KMj.js} +1 -1
  67. package/dist/updateCommand-CIoVDKnj.js +2 -0
  68. package/dist/{updateCommand-BMk2e4ky.js → updateCommand-CRJlAOaM.js} +139 -115
  69. package/dist/{webDev-BgWL9gKV.js → webDev-DSI9SOhs.js} +1598 -1028
  70. package/dist/webDev-DlvZO30c.js +2 -0
  71. package/dist/{webhooksCommand-CoIO3jbj.js → webhooksCommand-BvzXNHji.js} +1 -1
  72. package/dist/workspaceDeps-RKEkX92S.js +45 -0
  73. package/package.json +60 -19
  74. package/templates/AGENTS.core.md +2 -0
  75. package/templates/AGENTS.md +8 -4
  76. package/templates/agent-docs/_index.md +6 -4
  77. package/templates/agent-docs/_manifest.json +21 -5
  78. package/templates/agent-docs/ai.md +6 -6
  79. package/templates/agent-docs/authentication.md +73 -1
  80. package/templates/agent-docs/cli.md +6 -3
  81. package/templates/agent-docs/configuration.md +17 -0
  82. package/templates/agent-docs/data.md +522 -29
  83. package/templates/agent-docs/database/advancedqueries.md +8 -8
  84. package/templates/agent-docs/database/columntypes.md +2 -2
  85. package/templates/agent-docs/database/migrations.md +1 -1
  86. package/templates/agent-docs/database/querying.md +1 -1
  87. package/templates/agent-docs/database/schema.md +1 -1
  88. package/templates/agent-docs/database/seedsdialects.md +65 -3
  89. package/templates/agent-docs/database/transactions.md +3 -3
  90. package/templates/agent-docs/deployment.md +8 -0
  91. package/templates/agent-docs/internationalization.md +4 -2
  92. package/templates/agent-docs/introduction.md +32 -1
  93. package/templates/agent-docs/local-first-mobile.md +226 -30
  94. package/templates/agent-docs/observability.md +5 -1
  95. package/templates/agent-docs/plugins/atlassian.md +2 -2
  96. package/templates/agent-docs/plugins/audit.md +2 -2
  97. package/templates/agent-docs/plugins/auth.md +1 -1
  98. package/templates/agent-docs/plugins/billing.md +1 -1
  99. package/templates/agent-docs/plugins/cdc-out.md +8 -3
  100. package/templates/agent-docs/plugins/comments.md +164 -0
  101. package/templates/agent-docs/plugins/notifications.md +47 -4
  102. package/templates/agent-docs/plugins/presence.md +45 -3
  103. package/templates/agent-docs/plugins/prometheus.md +3 -1
  104. package/templates/agent-docs/plugins/queue.md +172 -0
  105. package/templates/agent-docs/plugins.md +17 -13
  106. package/templates/agent-docs/reference.md +35 -4
  107. package/templates/agent-docs/routing.md +585 -7
  108. package/templates/agent-docs/scheduling.md +1 -1
  109. package/templates/agent-docs/schema-driven-ui.md +226 -4
  110. package/templates/agent-docs/security.md +3 -3
  111. package/templates/agent-docs/templates/appshells.md +36 -4
  112. package/templates/agent-docs/whats-new.md +134 -66
  113. package/templates/apps/api-ai/package.json +6 -6
  114. package/templates/apps/api-auth/package.json +8 -8
  115. package/templates/apps/api-backend/package.json +7 -7
  116. package/templates/apps/api-backend-deactivation/package.json +7 -7
  117. package/templates/apps/api-backend-mail/package.json +8 -8
  118. package/templates/apps/api-backend-mariadb/package.json +9 -9
  119. package/templates/apps/api-backend-sqlite/package.json +8 -8
  120. package/templates/apps/api-backend-storage/package.json +8 -8
  121. package/templates/apps/api-cms/package.json +9 -9
  122. package/templates/apps/api-collab/README.md +3 -3
  123. package/templates/apps/api-collab/app.config.ts +1 -1
  124. package/templates/apps/api-collab/database/schema.ts +12 -8
  125. package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +1 -1
  126. package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +1 -1
  127. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +1 -1
  128. package/templates/apps/api-collab/package.json +8 -8
  129. package/templates/apps/api-collab/template.json +1 -1
  130. package/templates/apps/api-collab/tests/documents.setBody.test.ts +10 -2
  131. package/templates/apps/api-data-advanced/package.json +8 -8
  132. package/templates/apps/api-durable/package.json +8 -8
  133. package/templates/apps/api-feature-flags/package.json +9 -9
  134. package/templates/apps/api-governance/package.json +8 -8
  135. package/templates/apps/api-kv/package.json +8 -8
  136. package/templates/apps/api-moderation/package.json +8 -8
  137. package/templates/apps/api-observability/package.json +8 -8
  138. package/templates/apps/api-ratelimit/package.json +8 -8
  139. package/templates/apps/api-rbac/package.json +8 -8
  140. package/templates/apps/api-rest/package.json +7 -7
  141. package/templates/apps/api-row-history/package.json +8 -8
  142. package/templates/apps/api-saas/package.json +11 -10
  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 +9 -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/README.md +43 -24
  168. package/templates/apps/frontend-collab/app.config.ts +3 -3
  169. package/templates/apps/frontend-collab/package.json +14 -10
  170. package/templates/apps/frontend-collab/src/locales/de.ts +1 -2
  171. package/templates/apps/frontend-collab/src/locales/en.ts +1 -2
  172. package/templates/apps/frontend-collab/src/pages/page.test.tsx +72 -76
  173. package/templates/apps/frontend-collab/src/pages/page.tsx +45 -22
  174. package/templates/apps/frontend-collab/template.json +2 -2
  175. package/templates/apps/frontend-contact/package.json +7 -7
  176. package/templates/apps/frontend-dashboard/package.json +7 -7
  177. package/templates/apps/frontend-docs/content/docs/de/guides/first-page.md +4 -0
  178. package/templates/apps/frontend-docs/content/docs/de/intro/getting-started.md +4 -0
  179. package/templates/apps/frontend-docs/content/docs/en/guides/first-page.md +4 -0
  180. package/templates/apps/frontend-docs/content/docs/en/intro/getting-started.md +4 -0
  181. package/templates/apps/frontend-docs/package.json +9 -6
  182. package/templates/apps/frontend-docs/src/collections/docs.collection.ts +21 -0
  183. package/templates/apps/frontend-docs/src/locales/de.ts +0 -5
  184. package/templates/apps/frontend-docs/src/locales/en.ts +0 -5
  185. package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug]/page.tsx +17 -9
  186. package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +10 -5
  187. package/templates/apps/frontend-docs/src/pages/[locale]/page.tsx +8 -0
  188. package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.test.tsx +53 -15
  189. package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.tsx +27 -33
  190. package/templates/apps/frontend-docs/src/pages/page.test.tsx +17 -3
  191. package/templates/apps/frontend-docs/src/pages/page.tsx +16 -12
  192. package/templates/apps/frontend-i18n/package.json +6 -6
  193. package/templates/apps/frontend-landing/README.md +48 -0
  194. package/templates/apps/frontend-landing/app.config.ts +28 -0
  195. package/templates/apps/frontend-landing/package.json +7 -6
  196. package/templates/apps/frontend-landing/src/assets/hero.jpg +0 -0
  197. package/templates/apps/frontend-landing/src/fonts/Geist-Variable.woff2 +0 -0
  198. package/templates/apps/frontend-landing/src/fonts/LICENSE-Geist.txt +92 -0
  199. package/templates/apps/frontend-landing/src/globals.css +15 -0
  200. package/templates/apps/frontend-landing/src/globals.d.ts +17 -0
  201. package/templates/apps/frontend-landing/src/locales/de.ts +3 -2
  202. package/templates/apps/frontend-landing/src/locales/en.ts +5 -2
  203. package/templates/apps/frontend-landing/src/pages/page.test.tsx +79 -0
  204. package/templates/apps/frontend-landing/src/pages/page.tsx +26 -3
  205. package/templates/apps/frontend-landing/template.json +2 -2
  206. package/templates/apps/frontend-portal/package.json +8 -8
  207. package/templates/apps/frontend-saas/package.json +8 -8
  208. package/templates/apps/frontend-spa/package.json +7 -7
  209. package/templates/apps/frontend-ssr/package.json +7 -7
  210. package/templates/apps/frontend-ssr-api/package.json +8 -8
  211. package/templates/apps/frontend-static-blog/content/posts/cms-to-ssg.md +12 -0
  212. package/templates/apps/frontend-static-blog/content/posts/hello-static.md +12 -0
  213. package/templates/apps/frontend-static-blog/content/posts/islands-not-hydration.md +14 -0
  214. package/templates/apps/frontend-static-blog/package.json +9 -6
  215. package/templates/apps/frontend-static-blog/src/collections/posts.collection.ts +27 -0
  216. package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug]/page.tsx +7 -4
  217. package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +7 -1
  218. package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.test.tsx +19 -9
  219. package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.tsx +19 -21
  220. package/templates/apps/frontend-static-blog/src/pages/page.test.tsx +18 -5
  221. package/templates/apps/frontend-static-blog/src/pages/page.tsx +21 -14
  222. package/templates/apps/frontend-status/package.json +8 -8
  223. package/templates/apps/mobile-app/package.json +12 -11
  224. package/templates/apps/mobile-app/src/lib/deeplinks.ts +29 -17
  225. package/templates/apps/mobile-app/tests/deeplinks.test.ts +16 -0
  226. package/dist/agentsMd-SDDSkyl4.js +0 -2
  227. package/dist/apiBuild-BYBpL7Pz.js +0 -2
  228. package/dist/build-CPgcMQug.js +0 -793
  229. package/dist/codegen-CctkDO-1.js +0 -2
  230. package/dist/codegenCommand-DCdG2JN-.js +0 -137
  231. package/dist/dbCommand-DNb6yeOG.js +0 -2
  232. package/dist/doctorCommand-CqoWA2p5.js +0 -2
  233. package/dist/fileConventions-DOqD3lPS.js +0 -34
  234. package/dist/frameworkTableAssembly-C6ETawPR.js +0 -2
  235. package/dist/inspect-CuGDYES0.js +0 -2
  236. package/dist/manifestBuild-CPjhvM62.js +0 -2
  237. package/dist/renderModeScan-CcH2X1_D.js +0 -120
  238. package/dist/serveCommand-DLc-BznW.js +0 -2
  239. package/dist/start-DfL3fOiN.js +0 -3
  240. package/dist/updateCommand-5gFVfK5q.js +0 -2
  241. package/dist/webDev-CZbTsDcH.js +0 -2
  242. package/templates/apps/changelog/scripts/generate-rss.mjs +0 -38
  243. package/templates/apps/changelog/src/lib/releases.ts +0 -21
  244. package/templates/apps/frontend-static-blog/src/content/posts.ts +0 -64
@@ -1,4 +1,4 @@
1
- # What's new in 0.52.0
1
+ # What's new in 0.54.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,123 +9,191 @@ BREAKING entries name a codemod; run `voltro update` to apply it.
9
9
 
10
10
  ### ⚠ BREAKING
11
11
 
12
- - **@voltro/protocol, @voltro/runtime, @voltro/cli, @voltro/web, @voltro/plugin-storage, @voltro/plugin-ratelimit** The HTTP surface is complete seven gaps closed, each proven against the real listener.
12
+ - **@voltro/local-first** **`CrdtDocHandle.onUpdate` now tells its handler whether the update was LOCAL.** The signature gains a second argument: `onUpdate((update, { local }) => …)`. `local: false` means the blob came from folding somebody else's state through `applyState`.
13
13
 
14
- **Binary byte streams.** REST handlers return `bytes(stream, { contentType, contentLength?, contentDisposition? })` (lazy thunk form defers opening the source); plugin routes return `byteStream`. Piped, never buffered (a 256 MiB export streams with bounded heap), never compressed. Idempotency × stream is DECIDED: `streaming: true` on an idempotent method with an idempotency binding is a mount error (a stream cannot cache a replayable body retries would 409 until the TTL); an undeclared stream releases its claim at return. Storage's full-object download rides the generic path now.
14
+ Without it, an echo guard could not be written correctly on the public surface. Applying a peer's update fires the same handler a local edit does, so an app pushing from `onUpdate` re-broadcasts what it just received measured with three tabs open, one keystroke produced three server writes instead of one. It converges (the merge is idempotent), but the amplification scales with the session.
15
15
 
16
- **Negotiated compression + conditional GET.** brotli/gzip negotiated with a compressible-type allowlist, 1 KiB threshold, `Vary: Accept-Encoding` always on compressible types on the api's buffered responses AND `voltro start`'s HTML (`http.compression.{enabled,minBytes}`). The BREACH position is structural: `POST /rpc` responses are NEVER compressed. ISR keeps ONE uncompressed entry and compresses per hit. `voltro start` answers `If-None-Match` with 304 (weak md5 tags over the uncompressed body); REST routes can declare `etag: true` (GET, weak SHA-1 content tag).
16
+ The only app-level workaround was an `applying` boolean around `applyState`, and that is correct **only** while the backend emits synchronously a property `CrdtBackend` deliberately does not promise ("the backend decision lives behind our abstraction so it can change"). So the flag had to come from the backend: the yjs one tags its own folds with a symbol origin and reports anything else as local.
17
17
 
18
- **Raw WebSocket gateways.** `defineWebSocket({ path, auth, onConnection })` in a `*.ws.ts` file for FOREIGN protocols (a Yjs provider, a device fleet); app realtime stays the subscription protocol. `auth` is required with no default: `'subject'` runs the same chain as rpc/SSR BEFORE the upgrade (401 while it is still http) and binds the connection to the credential's expiry (close code 4001); `'public'` is a written-down decision. Every gateway path joins the upgrade origin guard (cross-site WebSocket hijacking 403). Teardown at construction; plain GET → 426; duplicate paths refuse the boot.
18
+ `codemod: none` — the parameter is ADDITIVE. An existing single-argument handler keeps compiling and keeps behaving exactly as before; there is nothing to rewrite. Read the flag when you push from `onUpdate`, which is what `useCrdtDoc` does for you.
19
+ - **@voltro/local-first** — **`RUNTIME_SEAMS` lists ONE seam now, not three** — `['sync-transport-app-tags']`. `RuntimeSeam` narrows with it. The two removed entries left in opposite directions, and `seams.ts` was asserting both halves of the contradiction at once: its `DONE` prose said the presence broker binding shipped while the array beside it still named `presence-broker-binding` as open.
19
20
 
20
- **Body caps everywhere.** The 8 MiB cap used to guard only `/rpc`; plugin routes read uncapped and webhooks read uncapped AND UTF-8-round-tripped (corrupting binary bodies fixed, proven byte-for-byte). `http.maxBodyBytes` (env `VOLTRO_MAX_BODY_BYTES`), per-route overrides on `defineRestRoute` and webhook handlers; a shared path takes its group's widest override. 413 for both request shapes.
21
+ - `presence-broker-binding` is BUILT. `usePresenceChannel` (`@voltro/plugin-presence/web`) rides the framework's own presence lane, so cross-replica fan-out belongs to the broadcast plugin and there is one presence wire rather than two; the shape is pinned by `presence/channelParity.test-d.ts`. - `wasm-sqlite-durable-adapter` was REJECTED, with the reasoning recorded in `mirror/queryMirror.ts`: the client's query surface is `(tag, input)` and predicates never exist client-side, so a browser SQL engine would evaluate a language the client never sees. A decision is not a gap, and listing it as one invites somebody to close it. A SQLite BACKING beneath `KvStore` remains available and is a different door.
21
22
 
22
- **Full method unions.** PATCH/HEAD/OPTIONS are first-class on plugin and REST routes; HEAD is admitted wherever GET is (RFC 9110) with the transport dropping the body.
23
+ `codemod: none` because nothing in the framework reads this constant and nothing asks a user to: it is a documentation manifest that happens to be typed. There is no mechanical rewrite for a removed member of one — the correction is the sentence above.
23
24
 
24
- **BREAKING the interceptor chain is fail-closed.** A throwing `onHttpRequest` interceptor is a 500 + a log line now; it used to be swallowed, which let a crashed security gate silently stop guarding. The manual codemod tells interceptor authors where the decision lives: propagate (a gate) or catch-and-degrade-loudly (protection with a dependency) — plugin-ratelimit's httpShield now does the latter, so a Redis outage cannot become a self-inflicted API outage. No app-authored call sites change shape, hence `apiSurface: compatible` — the break is the error POLARITY of one plugin-author hook, carried by the manual note.
25
+ The docs site's "what's shipped vs. a runtime seam" table carried the same two rows in both languages and now carries one, with both departures stated rather than silently dropped: "we have not built it" and "we decided against it" are different answers and a reader is entitled to know which one applies.
25
26
 
26
- **Middleware response headers + CSP nonce.** `middleware.ts` can return `responseHeaders` (applied on every render response shape, both boot paths; prerendered static files are the documented proxy-side limit) and `cspNonce` — the framework stamps every script tag of that render (React's own included) while the policy header stays the middleware's. `isr` + `cspNonce` is refused loudly: a cached nonce is a lie the browser enforces.
27
+ ### Added
27
28
 
28
- Deliberate limits: no multipart parser on REST/webhook routes (the storage upload routes are the sanctioned file path); static-file response headers belong at the proxy.
29
- - **@voltro/plugin-row-history, @voltro/cli** — `@voltro/plugin-versioning` is renamed to `@voltro/plugin-row-history` — the name now says what it does.
29
+ - **@voltro/content, @voltro/cli** **Relative images inside markdown content are copied into the build, and their `src` rewritten to the copy.** `![cover](./cover.png)` now lands in `dist/assets/content-media/<hash>.<ext>` and the rendered HTML points there. `voltro dev` serves the same URL shape on demand from the source file, so a page's HTML does not change between development and a build.
30
30
 
31
- The old name collided head-on with API versioning (versioned REST routes, `/v1` `/v2`, sunset flow), which `defineRestRoute` now supports as a first-class `version:` field. What this plugin does is row history + time travel (`rowHistory` / `rowAsOf` / `restoreAsOf` / `diffVersions` over `_voltro_row_history`); every comparison table that filed it under API versioning was reading the name, not the feature.
31
+ Nothing moved those files before: the artifact emit wrote JSON and the renderer emitted the relative src verbatim, so a built blog asked the browser for a path that exists only in the source tree while `routing/assets.md` said, in both languages, that the content pipeline copied and hashed them.
32
32
 
33
- Renamed with it: the factory (`versioningPlugin` `rowHistoryPlugin`), the options type (`VersioningPluginOptions` `RowHistoryPluginOptions`), and the default instance alias (`versioning` `row-history` — the inspect endpoint path and boot-log lines; an explicit `alias:` you passed is untouched). The table name (`_voltro_row_history`) and `VOLTRO_ROW_HISTORY_TTL_HOURS` already carried the new name: no data movement, no env change, no migration.
33
+ A reference to a file that does not exist now FAILS the build and names the path. The alternative emit the src and continue is the defect itself: the page renders, the browser 404s, and the build says nothing.
34
34
 
35
- The codemod rewrites imports, the factory call sites and the options-type references, and prints the one step it cannot do swapping the dependency in package.json.
35
+ Absolute (`/…`) and remote sources are untouched; they are not the build's to move. Copy and content-hash only — build-time transformation (resize / format) remains a named non-goal for markdown content, because a markdown reference carries no width and no `sizes` to derive one from. The `?image` pipeline stays the answer where that matters.
36
36
 
37
- ### Added
37
+ Renderers outside the framework's build are unaffected: `@voltro/content` exposes this as a registered resolver (`setContentAssetResolver`), and with none registered a relative src is left exactly as written rather than rewritten to a path nothing serves.
38
+ - **@voltro/cli, @voltro/runtime** — **The server-side CRDT compaction threshold is an `app.config.ts` field now, not an environment variable only.** `crdt.compactMaxBytes` (default 512 KiB, `0` disables) decides when a merged `crdtText()` / `crdtDoc()` blob is soft-compacted — re-encoded through a live doc, same lineage, so every outstanding client update still merges. `VOLTRO_CRDT_COMPACT_MAX_BYTES` still wins over it: an operator acting on a running deployment outranks what the project declared.
38
39
 
39
- - **@voltro/cli** `voltro doctor` now reports every declared `@voltro/*` dependency with no import site the residue a migration off a framework package leaves in `package.json`, where it keeps getting installed, walked by `voltro update`, and read as evidence the package is in use, its breaking-change notes included.
40
+ This is the standing rule ("every number the framework picks on your behalf is a config field with a default, plus an env override"), applied to the one knob that had only the environment half. A threshold sized for a document shape is a property of the project, so it belongs in a file a reviewer reads and a deploy carries not in whatever `env:` block somebody remembered to set.
40
41
 
41
- Scoped to `@voltro/*` deliberately (third-party packages have too many legitimate no-import shapes), and three states are distinguished and printed: exempt by name with a reason (`@voltro/cli` is the binary, `@voltro/devtools` is mounted by `voltro dev`, `@voltro/sql-*` drivers are loaded from config); not-measurable-yet for `@voltro/client`/`@voltro/web` on a tree where codegen has never run (a missing measurement, not a dead dependency); and unimported advisory, never fatal. A mention in a comment or an error string does not count as an import, and a commented-out import counts least of all: it is the artefact the rule exists to see past. Also in `voltro doctor --json` under `unimportedDeps`, `null` when no `package.json` could be read.
42
- - **@voltro/web, @voltro/cli** — Islands now save real bytes — every `interactive: 'islands'` page gets its OWN browser entry.
42
+ Resolved by ONE builder both boot paths call (`wireCrdtTunables`), registered into the process slot the merge path reads the `wireReactiveSocketTunables` shape, for the reason that shape exists: two paths resolving a value separately is how they come to disagree. `setCrdtCompactMaxBytes` was exported and called by nothing before this; the runtime kept a lazy env read for a process that never runs a boot path (a unit test), and that remains the fallback rather than a second answer.
43
43
 
44
- Measured on the reference fixture (pinned in `bundle-budget.json`, 2026-08-25): an islands page's first load is **59.6 KB gz** against **181.9 KB gz** for a fully hydrated pagereact + the island runtime + that page's islands, no router, no Effect runtime, no subscription cache. The bundle-budget gate pins a hard <70 KB bound AND the ratio (<50 % of a full page), so a regression that re-couples the entries fails loudly.
44
+ Note `0` is a value here, not an absence it means "do not compact"so the resolver's floor is `>= 0` on both the config and the env side. The `> 0` floor the other tunables use would have silently dropped a declared zero.
45
+ - **@voltro/cli** — **The gRPC surface's shutdown drain budget is configurable — `grpc.drainMs` (default 5000, `0` forces immediately, env `VOLTRO_GRPC_DRAIN_MS`).** It was a `5_000` literal in the `stop()` closure.
45
46
 
46
- - **`@voltro/web/islands`** is the new react-only subpath `island()` and the hydration runtime import only react + react-dom/client. Importing the `@voltro/web` BARREL (or `@voltro/i18n`) anywhere in an island's import graph is now a BUILD error naming file and specifier: an island hydrates provider-less, router hooks and `useT()` throw there anyway, and the barrel would pull the Effect runtime into the slim entry. - **The build finds each page's islands** through its relative import graph (transitively, through components in between) and emits one shell + entry per islands page. `interactive` must be a source LITERAL to select the slim entry a computed value ships the full entry as before, and the build says so. - **All three paths**: `voltro build` (SSG renders into the per-page shell, main-shell stylesheets folded in), `voltro start` (ssr/isr islands routes serve their shell), `voltro dev` (same mechanism on demand — violations fire in dev, not first in CI). Fixed on the way: a STATIC islands page fell through dev's render gate to the SPA fallback and loaded the full app entry — dev now server-renders it like production. - **Framework islands**: an island importing `@voltro/client` (`useSubscription`, …) is detected — that page's entry boots the rpc client and wraps each island root in `VoltroRuntimeProvider`, so the island receives live data. Presentational pages never pay for the client core. - **`hydrate: 'only'`** (Astro's `client:only`): the server renders an empty placeholder — a browser-only lib touching `window` in render no longer crashes the SSR pass — and the client mounts fresh with `createRoot`. - **Island props are declared lossy where they are**: props cross an HTML attribute as JSON, so a `Date` arrives as an ISO string and `Map`/`Set`/ functions do not arrive at all — dev warns naming the island and prop. - The per-island `manualChunks` rule is gone: with per-page entries as additional rollup inputs it MERGED the shared react modules into the island group (a second-React-instance shape); per-island chunks were also redundant — the entry already scopes to the page's islands, and they were statically preloaded anyway.
47
+ It is a knob rather than a measured constant, and the distinction is worth stating because the framework deliberately refuses knobs elsewhere on the same page (compression levels are fixed "a knob nobody can pick correctly is worse than a measured default"). A compression level has no input outside the process. A drain budget has two, and neither is ours: the orchestrator's termination grace, past which SIGKILL arrives and a longer budget is decorative; and the app's longest legitimately in-flight call, below which every rolling deploy force-closes work that would have finished. The default fits the 30 s grace both kubernetes and `docker stop` default to.
47
48
 
48
- Limits: an islands page reached via SPA navigation from a full page runs in the already-loaded app bundle (the saving applies to hard loads of the islands page); a page's islands share one entry (hydrate strategies control WHEN each hydrates, not when it downloads).
49
+ The `grpc:` config shape was also declared THREE times `ApiAppConfig`, `ServeApiOptions` and the shared builder's options which is how a field lands on two of them. It is one exported `GrpcSurfaceConfig` now, referenced by all three, so the shared builder both boot paths already call is the only thing that reads it.
49
50
 
50
- Why the golden churn is compatible: `hydrate: 'only'` widens a union, `hydrateIslandsOnPage` gains an optional options argument, and the subpath is a new export.
51
- - **@voltro/ui, @voltro/client, @voltro/web, @voltro/cli** — Forms without JavaScript`<AutoForm>` on a server-rendered page now works with JS disabled, end to end.
51
+ The boot line and the budget-exceeded warning both name the resolved value, so what a process will wait is visible before the shutdown rather than after it.
52
+ - **@voltro/cli** — **Partial prerendering's counters are now real metrics they were incremented by both boot paths and read by nothing, while the observability docs listed them among the exportable ones.** A reader who went looking for them at the OTLP or Prometheus endpoint found nothing there: `pprMetrics()` was a `globalThis` counter slot with no registry bridge, no inspect endpoint and no log line.
52
53
 
53
- The form always renders `action="/form/<mutationTag>"` + `method="post"`; with JS alive, `onSubmit` intercepts exactly as before (optimistic rpc path unchanged). The `/form/*` endpoint is mounted by the WEB listener on BOTH boot paths (`voltro dev` and `voltro start`, one shared builder):
54
+ Five series, all labelled `page` — the DECLARED route pattern (`/blog/[slug]`), never a resolved URL: `voltro_ppr_shell_serves_total`, `voltro_ppr_hole_passes_total`, `voltro_ppr_hole_settles_total`, `voltro_ppr_hole_errors_total`, and the `voltro_ppr_hole_pass_seconds` histogram. They land in Effect's global `MetricRegistry`, so `@voltro/plugin-prometheus`' `GET /metrics` and `GET /_voltro/inspect/metrics` both see them with nothing to register the same placement `@voltro/database` uses for `voltro_db_*`.
54
55
 
55
- - **Origin-checked at the door** with the same `classifyRequestOrigin` the api's rpc listener runs — a cross-origin form POST is a 403 before a byte of the body is parsed. (The server-side forward reaches the api as a no-browser-origin request, so the web listener's check is the one that guards this surface.) - **One validation path.** The urlencoded body maps through the schema-driven `formDataToInput` (checkbox present/absent true/false, `''` on number/date omits the field — never a silent 0 —, repeated keys → arrays, non-numeric strings pass through RAW so the decode fails honestly instead of minting NaN, unknown keys dropped) and validates with the SAME `validateFields` the client-side submit runs — byte-identical field errors. - **PRG on success**: 303 back to the submitting page, or to `<AutoForm redirectTo>` (same-origin relative paths only — anything else is refused, a hidden field must not become an open redirect). Reloading the redirected-to page cannot resubmit. - **422 re-render on validation failure**: the referring page renders in the same response with field errors + submitted values in the SAME error UI (`role="alert"`, aria unchanged), `cache-control: no-store`, bypassing the ISR cache in both directions. An rpc refusal after valid input (guard, server error) renders as a form-level error. The flash also embeds as a JSON script, so a page whose bundle arrives late hydrates to the identical state. - **Valid submits forward server-side over `POST /rpc`** with the request's cookie — auth middleware, guards and the rpc interceptor chain run identically to every other mutation.
56
+ `voltro_ppr_hole_errors_total` is the one to alert on, and it is a separate series rather than a `status` label for a reason: a failed hole pass is invisible from outside. The shell is already on the wire with a `200`, so the page renders and every `<Await>` boundary silently stays on its fallback forever.
56
57
 
57
- New `<AutoForm>` props: `formKey` (several forms per page the 422 re-render re-fills only the submitted one), `redirectTo`, and `action={false}` for purely static deploys where `/form/*` does not exist. Headless: `useFormBinding` gains `flash` + `formError`, `@voltro/web` gains `useFormFlash(formKey)`.
58
+ Two things changed beyond the bridge. The last/max latency PAIR is gone in favour of the histogram a last value plus a monotonic max answers strictly less, and the max never comes back down. And `voltro dev` now counts shell serves too; the counter previously existed only on the `voltro start` path, which for a metric is the silent kind of drift (the series simply reads zero).
58
59
 
59
- Also fixed on the way: a required `Schema.Boolean` field with no default used to block the JS submit as "missing" while its checkbox rendered visibly unchecked boolean fields now seed `false`, agreeing with what the user sees (and with the no-JS mapping).
60
+ The `globalThis` slot is deleted rather than kept beside the registry: two counters for one fact is how a dashboard and a scrape target come to disagree.
61
+ - **@voltro/plugin-presence** — `resolveMember` now receives the app's `store`, and `identityFields:` closes the hole in the obvious resolver.
60
62
 
61
- Deliberate limits: the no-JS error re-render needs an `ssr`/`isr` page (a static page cannot be re-rendered with request state; a minimal error page is the fallback); a purely static deploy has no `/form/*` endpoint (use `action={false}`); file uploads stay JS-only (multipart → 415). On an ssr page pass `schema` explicitly — descriptor resolution is a client-runtime feature and the SSR render would otherwise show no fields.
63
+ Both from a deployment that adopted the hook and reported what it cost them.
62
64
 
63
- Bundle note: the `serverContext` chunk group is renamed `serverRequest` by the context's move to @voltro/client identical 190 B gz, re-pinned in `bundle-budget.json` (fresh full measurement 2026-08-25: firstLoad 185,494 B gz, was 185,309 +185 B from the form-flash read in the request context).
65
+ **The store.** The resolver's whole job is a by-id read, and `app.config.ts` where it is declaredis evaluated long before a migrated `DataStore` exists. The workaround is a module cell filled from a `*.startup.ts`; the plugin already receives the store through `bindDataStore`, so it hands it to the resolver instead: `resolveMember: ({ subject, store }) => …`.
64
66
 
65
- Why the golden churn is compatible: every addition is a new export or an optional prop; `FormBinding.formError` is a new member of the hook's RETURN type (nothing in the public API accepts a caller-built `FormBinding`), and `ServerRequestContextValue.formFlash` is optional.
66
- - **@voltro/runtime, @voltro/cli, @voltro/cms, @voltro/plugin-broadcast** — On-demand ISR revalidation — the third invalidation axis next to time (`revalidate`) and CDC (`cacheInvalidatesOn`). Server code in the api process calls `revalidatePath('/blog/[slug]')` / `revalidateTable('posts')` / `revalidateTag('pricing')` (exported from `@voltro/runtime`; callable from mutations, actions, webhook receivers and REST routes) and the matching ISR cache entries fall on EVERY `voltro start` replica — including on dialects with no CDC at all, which is the case this exists for.
67
+ **The hole.** Resolved fields merge OVER the caller's `meta`, so a key the resolver does not return keeps whatever the client sent. A resolver returning only what it found — `{ userName }` for a user with no avatar — therefore leaves a caller-supplied `avatarUrl`, or a `userName` the resolver has never heard of, standing in the roster every other member reads. That is the exact substitution the hook exists to prevent, and the doc comment's promise ("a caller cannot override what the server says about them") was broader than the behaviour: it holds only for the keys returned on that call.
67
68
 
68
- Transport is dialect-shaped, either or both: on postgres a `pg_notify` rides the SAME LISTEN connection the CDC invalidator already holds (no broker needed); everywhere else the broadcast broker carries it (`BROADCAST_URL` on both deployments; channel namespaced by `VOLTRO_BROADCAST_NAMESPACE` deliberately env-derived, because this channel pairs an api with its WEB app and no shared name is derivable). A web process with isr routes and neither transport warns loudly at boot; under `voltro dev` the calls are documented debug-logged no-ops. Tags share ONE mechanism with tables — `cacheInvalidatesOn` accepts free strings, so `revalidateTag` is the same sink under another name.
69
+ `identityFields: ['userName', 'avatarUrl']` names the keys the SERVER owns. They are stripped from the caller's `meta` BEFORE the merge, so the answer is the same on every call whether or not the resolver produced a value. Ignored without a `resolveMember` with no server identity to protect, stripping a client field would only delete data the app put there deliberately. Returning every identity key (`null` where you have no value) remains the alternative, and is what the reporting deployment did.
70
+ - **@voltro/plugin-queue** — **Queue consumers now export Prometheus series and open one tracing span per message, continuing the producer's trace.** Both were specified and neither was built: `voltro_queue_consumed_total` had zero occurrences anywhere in the repo, and `traceparent` was copied out of the message headers onto `ctx.traceparent` — a value a handler can forward by hand, not a trace. A Kafka hop was where every distributed trace ended.
69
71
 
70
- Correctness edges built in: `revalidatePath` against a `static` route is a NAMED error on the web process (never a silent no-op); a purge landing while an SWR refresh or miss fill renders is guarded by a per-key generation counter on BOTH cache backends — the pre-purge page cannot be written back with a full TTL, and a refused write also suppresses the postgres backend's fire-and-forget upsert so no replica resurrects a deleted row. A content type declares `revalidate: { paths, tags }` and `publish()`/`unpublish()` fire them after commit.
72
+ Four series, all in Effect's global `MetricRegistry` (so `GET /metrics` and `GET /_voltro/inspect/metrics` both see them): `voltro_queue_consumed_total{topic,outcome}`, `voltro_queue_retries_total{topic}`, `voltro_queue_produced_total{topic}` and the `voltro_queue_lag_messages{topic,partition}` gauge.
71
73
 
72
- Proven end-to-end (`scripts/revalidate-e2e.mjs`): 1 api + 2 `voltro start` replicas behind Redis (warm purge both fresh, with a negative control), the sqlite dialect leg, and a broker-less postgres leg where the NOTIFY line alone carries the purge to a LISTEN-only replica.
74
+ `outcome` is a closed two-value union — `ok` and `dead-lettered` together are every message the runner finished with, so the dead-letter rate is a division with no second series to join. A message abandoned by a REBALANCE is deliberately in neither: it was not consumed here, its new owner redelivers and counts it there, and counting it twice would make that ratio wrong in the direction of looking healthy.
73
75
 
74
- `apiSurface: compatible` additive only: the new `@voltro/runtime` revalidation exports, an optional `revalidate` on `ContentTypeSpec`, the optional `onRevalidate`/`revalidateChannel` on the CDC invalidator options, a widened `BroadcastChannelKind`, and `IsrCache.set`'s new optional generation guard (plus `generation()`).
75
- - **@voltro/protocol, @voltro/plugin-openapi** — `defineRestRoute` takes an opt-in `version:` — versioned REST APIs with a sunset flow.
76
+ Lag costs no extra round trip `highWatermark` already rides along in the fetch response, so it is arithmetic on data the provider was handed. It is recorded in the provider rather than the runner because that is the only layer holding the watermark; the runner could only get it by asking the broker. Offsets are parsed as `BigInt` so a long-lived topic cannot lose precision, and an unparseable pair records nothing rather than a zero: "no lag" and "we could not tell" must not read the same.
76
77
 
77
- `version: 'v2'` + `path: '/customers'` mounts the route at `/v2/customers`, the same `/vN/` convention the `publicApi:` projection and the built-in `/v1/api-keys` surface already use. The path is normalised ONCE at definition time, so every consumer the mount, path-param matching, the idempotency scope, tracing spans, the OpenAPI generator — sees the mounted path and none can disagree.
78
+ The per-topic inspect counters stay, and each fact is now moved by exactly ONE recorder that writes both the slot and the series, so the dashboard and the scrape target cannot drift.
78
79
 
79
- Deliberate edges:
80
+ Each message is processed inside a `queue.consume` span whose parent is the incoming `traceparent`, parsed by the same `externalSpanFromTraceparent` the inbound-webhook path uses — so a malformed or all-zero header means "no parent" (a fresh root span), never a failed message. The span covers the whole message including retries and the dead-letter publish, and carries the OTel `messaging.*` attributes plus `voltro.queue.outcome`. The topic is an attribute, not part of the span name.
80
81
 
81
- - **Opt-in, no auto-prefix.** A route without `version:` keeps its literal path untouched an automatic prefix would silently move every deployed route. A path that already starts with `/vN/` AND declares `version:` is refused loudly at definition (both spellings at once is never what the author meant). - **Two versions are two descriptors.** The old version is ordinary code — visible, testable, deletable — carrying `deprecated:` (the replacement pointer) and `sunset:` (the date it starts answering `410 Gone`; the 410 body now also names which `version` died). - **One OpenAPI document for every version.** The `/vN/` prefix already separates the paths; each versioned operation is grouped under a version tag and carries `x-voltro-api-version` for tooling. No `?version=` filtered spec — a second document shape for information the paths already state. - The rpc socket stays outside URL versioning on purpose: the generated client is versioned with the server it was generated from. A stale browser tab runs the previous client until reload — that skew window exists and is documented, not solved by URLs.
82
- - **@voltro/web, @voltro/cli** — Opt-in View Transitions for SPA navigations. `router.viewTransitions: true` in a web `app.config.ts` runs every route swap `<Link>` clicks, `navigate(...)`, back/forward through `document.startViewTransition`; individual navigations override the default in either direction with `navigate(to, { transition })` / `<Link transition>`.
82
+ Known limit, measured rather than assumed: a span opened from DETACHED work does not reach an OTLP exporter, because the framework's tracer is a `Layer` provided only inside the rpc server's scope. This is framework-wide (`cdcOut.deliver` and `plugin.<name>.schedule-fire` are the same shape) and is stated in the queue docs rather than implied away. The metrics are unaffected the metric registry is a process global.
83
+ - **@voltro/runtime, @voltro/voltro** — `setRowFilter({ …, tables: ['bookmarks', 'recentSearches'] })` — declare which tables your filter may narrow, and delta-resume survives everywhere else.
83
84
 
84
- The visual swap is the router's deferred-navigation commit, flushed synchronously inside the transition callback by that point the target's lazy chunk and loaders have settled, so the flushed tree renders with data in hand. Three deliberate behaviors: `defer()` fields (and an explicit `Pending` skeleton's settled content) resolve AFTER the transition as ordinary updates, never a second animation; navigating while a transition is animating skips the running one (last navigation wins, nothing queues); overlay/dialog state changes never trigger one — a root snapshot would cross-fade the whole viewport for a one-layer change.
85
+ Delta-resume is excluded for a subscription whose row set is re-resolved per delivery: replaying deltas could serve rows the subject has since lost. But the question the runtime could ask was only "is a filter registered in this process?", so ONE registration disabled cheap reconnects for every subscription in the app a deployment measured a filter narrowing 4 tables costing the feature on all 173 of their query descriptors, 55 of whose source tables the filter never touches.
85
86
 
86
- Fallback is exact: a browser without the API, and any user with `prefers-reduced-motion: reduce`, gets today's untransitioned swap same timing, nothing to feature-detect. Styling is plain `::view-transition-*` CSS (no animation DSL); cross-document transitions for static/MPA pages are a one-line `@view-transition` CSS opt-in with no framework involvement.
87
+ The exclusion is per table now. A declared filter keeps resume for every subscription whose source is not in its set; an undeclared filter keeps today's conservative behaviour (the runtime cannot know which tables the predicate may reach, and "unknown" must read as "yes").
87
88
 
88
- Proven in a real chromium (`scripts/browser-view-transitions.mjs`): called on navigation, silent under reduced-motion, harmless with the API deleted, one transition across a `defer()` commit, rapid double-navigation lands on the last targetplus the jsdom wiring suite and the generated-entry flag check shared by all three web boot paths.
89
+ **Declared, not probed** and the difference is soundness, not taste. Resolving the scope at subscribe and treating `predicate(ctx, source) === undefined` as safe is the cheaper version: it is wrong, because the predicate is a function of freshly loaded context, so a table it does not narrow now may be narrowed on the next deliverywhich is the entire reason the filter is re-resolved per delivery. A static list is a promise about every future resolution, and the runtime holds the app to it: a predicate returned for an undeclared table raises the new `RowFilterDeclarationViolated` at the read that did it (the request fails, a subscription is revoked) rather than serving rows under a resume grant the declaration no longer earns.
89
90
 
90
- `apiSurface: compatible` additive only: a new optional `viewTransitions` on `RouterProps`, optional `transition` on `NavigateOptions`/`LinkProps`, and the optional `router` block on the web app config.
91
- - **@voltro/web, @voltro/cli** — Schema-typed search params — the query-string half of the URL is now part of the type graph.
91
+ Eager loads (`.with(...)`) stay excluded wholesale a relation resolves below the seam that narrows (see the sibling entry).
92
92
 
93
- A page declares its contract once:
93
+ `apiSurface: compatible` the golden also picks up `subscribeDescriptor` gaining its `reauthorize`/`refilter` parameters, which landed in the SSE/gRPC row-filter fix without a regeneration. That member is a framework-internal transport binding: the only implementors are the two boot paths (`serveApi.ts`, `dev.ts`), both already passing the new arguments. No user code implements or calls it, so no code that compiled stops compiling.
94
+ - **@voltro/local-first** — **`useCrdtDoc` — the transport half `useCrdtEditor` had no partner for.** `useCrdtEditor({ doc })` takes a `CrdtDocHandle` and owns the editor lifecycle; getting that handle wired to a server was app-level glue until now. Import it from `@voltro/local-first/react`:
94
95
 
95
- ```ts
96
- export const searchParams = Schema.Struct({
97
- q: Schema.optionalWith(Schema.String, { default: () => '' }),
98
- page: Schema.optionalWith(Schema.NumberFromString, { default: () => 1 }),
96
+ ```tsx
97
+ const shared = useCrdtDoc({
98
+ cell: { table: 'documents', id, column: 'body' },
99
+ remote: row.data?.body ?? null, // the reactive query streaming the row
100
+ push: (w) => save.mutate({ id: w.id, update: w.update }),
99
101
  })
102
+ // in a CHILD component, so useCrdtEditor is never a conditional hook call:
103
+ const editor = useCrdtEditor({ doc })
100
104
  ```
101
105
 
102
- and gets, end to end:
106
+ It returns `{ doc, loaded, outstanding, synced, setOnline }`. `doc` is `null` until the mount effect has run — the document is built in an effect, never during render, which is what a rich-text binding gets wrong first (constructing during render crashes a prerender and leaves a second instance alive under StrictMode).
103
107
 
104
- - **Typed reads** `useSearchParams(searchParams)` returns the decoded, defaulted shape, SSR-aware; the zero-arg call keeps returning the raw `URLSearchParams`. Decoding is TOTAL: an invalid query falls back to the schema's defaults instead of crashing a render; only a schema that cannot even decode `{}` (a required field with no default) throws, naming the fix. - **Typed links** — the generated `routes` builder brands the route's URL with the schema's shape through a TYPE-ONLY page import (zero value edges: code-splitting is untouched, pinned by test), and `withQuery` type-checks against it — a misspelt key or wrong value type is a compile error. The link-side encode is canonical and schema-free (strings/numbers/booleans, arrays as repeated keys); a roundtrip test pins that it produces exactly what the schema's decode accepts. Deliberately ONE generic signature rather than overloads: with overloads, a wrong key would silently fall through to the permissive untyped form and the compile error would never fire. - **Typed writes** — `useSetSearchParams(searchParams)` returns the typed setter: its object form replaces the query (same semantics as the untyped form), and its updater form receives the CURRENT decoded params, so keeping `?filter` across a page flip is one explicit spread — `setParams((p) => ({ ...p, page: p.page + 1 }))` instead of a hand-rolled merge. - **A fail-closed isr gate** `renderMode: 'isr'` plus a `searchParams` export is refused at boot: the isr cache is keyed by path (+tenant+locale), not query, so the first variant would be cached for every query — and the gate catches the re-exported spelling (`export { searchParams } from …`, the mirror-route pattern) too, not only the local declaration. - **A doctor rule** — a page that exports the schema but keeps reading the query with zero-arg `useSearchParams()` is flagged with the typed spelling.
108
+ **It is `createSyncClient`-backed, not new glue.** The four-line hand-rolled version loses the offline queue, the bounded retry, `outstanding`/`synced`, the per-cell coalescence that drains 1000 offline keystrokes as O(1) pushes, durable persistence, and the discipline that a `null` row means NOT LOADED rather than an empty document fold an empty document over a loading row and the first keystroke can push a state that erases what was stored.
105
109
 
106
- Deliberate limits: array fields decode a single occurrence as a one-element array (link shape stays stable); `siblingApps` routes stay untyped (their schemas live in a foreign compile graph); `static` pages see the defaults at build time and decode live on the client.
110
+ **The echo guard is the part that could not be written outside the package until now.** A `crdtDoc()` handle is mutated by the EDITOR, so local edits arrive as `onUpdate` callbacks — and folding a peer's state through `applyState` fires the same callback. The hook pushes only when `local` is true. Without that, every client re-broadcasts what it just received.
107
111
 
108
- Why the golden churn is compatible: `VoltroRouteUrl` gains a type parameter with a DEFAULT (`<TSearch = unknown>`), so every existing bare `VoltroRouteUrl` spelling still compiles, and the new brand member is an OPTIONAL phantom property of type `unknown` assignability in both directions is unchanged. `withQuery`'s parameter for the untyped case is strictly WIDER than before (adds `boolean` and array values); no call that compiled stops compiling.
112
+ `useCrdtText` and `useCrdtDoc` now build their sync client through one shared internal seam rather than two copies of the same transport wiring.
109
113
 
110
114
  ### Fixed
111
115
 
112
- - **@voltro/cli** — `voltro agents-md`'s copied `agent-docs/` mirror no longer keeps orphaned modules across a re-seed.
116
+ - **@voltro/runtime** — A row filter was bypassed for any table reached through `.with(...)`.
117
+
118
+ The filter is AND-merged onto a read's BASE table by the store middleware; an eager load is resolved BELOW that seam — the memory store recurses through its own raw read, the SQL stores fold the relation into one join — so the relation's rows never passed the code that would narrow them. Measured on one data set: a filter restricting `readers` to the caller returned exactly the caller's row on a direct read and BOTH rows through `.with({ readers: true })`.
119
+
120
+ Applying the filter inside eager compilation is the real fix and it is a per-dialect change. Until then the read REFUSES rather than serves, naming the table and both ways out (read it as its own query, or drop it from the `.with(...)`). Only relations reaching a table the filter actually narrows are affected; every other eager load is untouched, and an app with no filter pays nothing.
121
+
122
+ Silent exposure is the one outcome that must not survive the gap — the same reasoning that makes this module refuse to fail open when `load` fails.
123
+ - **@voltro/cli** — **The AGENTS.md seeder executed every app's `app.config.ts`, so seeding one app could stop another from booting.** It walked `apps/<proj>/<app>` for the whole workspace and `import()`ed each config to read its `plugins:` list. An `app.config.ts` is not inert: it imports the app's schema, which calls `databaseHandle(...)`, which REGISTERS every table. Two apps that each declare an `actors` table therefore collided —
124
+
125
+ ```
126
+ duplicate table 'actors' registration: two different table descriptors claim the same name.
127
+ ```
128
+
129
+ — and the app being booted was the one that failed. A reference project could not start at all.
130
+
131
+ The seeder parses the config now instead of importing it, resolving each plugin's package by pairing the identifiers called inside the `plugins: [...]` block with the import that introduced them (so a renamed import still lands on the right package), and reading the `{ name: '…' }` form directly. A plugin it cannot attribute is dropped rather than guessed: the index is a filter, and a wrong row is worse than a missing one.
132
+
133
+ The rule this encodes: **a step that writes documentation must not execute application modules.** Nothing about composing an index needs a config's runtime value, and the sibling app-discovery pass was already reading the same file as text for its `type:` check.
134
+
135
+ Worth knowing for the next diagnosis: two different descriptors for one name reads like one module evaluated twice, so the investigation went looking for split module identity (pnpm symlink vs real path, ESM cache keying). It was two DIFFERENT apps, pulled in by a documentation step — found by tracing what actually resolved, not by reasoning about what could.
136
+ - **@voltro/cli** — **`voltro build` read the PREVIOUS build's config, so every `app.config.ts` change landed one build late.** `loadConfig` prefers the precompiled `.framework/dist/server/appConfig.js` when it exists — correct for `serve` / `start`, which cannot read TypeScript, and exactly wrong for the command that WRITES that file. Anything consumed before the config is recompiled took the stale value: `fonts`, `images`, `seo`, `theme`, `locales`.
137
+
138
+ Silent in the worst way: the second build is always right, so the symptom is "my change did nothing" followed by "…and now it works", with no error either time. Measured by setting `title` to a probe value, building, and finding the old title in the generated shell.
139
+
140
+ `voltro dev` already carried this fix, with the reasoning written on the option itself. The build path is the one that makes the artefact, so it is the last place that should trust it.
141
+ - **@voltro/local-first** — **`useCrdtEditor` built its editor during RENDER, so a default page could not mount it and StrictMode leaked one.** The Tiptap instance was constructed inside a `useMemo`, which runs while rendering. Two consequences, both real:
142
+
143
+ - **Server render threw.** Tiptap needs `window`; a page mounting this hook failed prerender with `there is no window object available`. `renderMode` defaults to `'static'`, so that is the ordinary page, not an exotic one. - **React's double-invoked render built TWO editors** and the cleanup destroyed only the last, leaving the first alive holding its Yjs binding. `useCrdtText`'s own comment documents exactly this shape as a bug; the editor had it anyway.
144
+
145
+ Construction moved into an effect, and the cleanup destroys THAT instance rather than whatever the ref currently holds — reading the ref destroys the new editor on a rebuild and leaves the old one running.
146
+
147
+ `extensions` is no longer a dependency. Callers pass an inline array literal, which is a fresh identity every render, so listing it rebuilt the whole editor per keystroke; it is read at construction from a ref instead. The trade is stated rather than hidden: changing `extensions` after mount does not rebuild the editor — remount with a `key` if you need that.
148
+
149
+ **`CrdtDocHandle` is exported now.** It is the parameter type of `useCrdtEditor`, and it was declared, used across the package, and exported from nowhere — visible in the built `.d.ts` only as a bare `declare interface`, which no import can reach. An app binding an editor could not type its own variable.
150
+ - **@voltro/cli** — `middleware.ts`'s `cspNonce` now reaches the scripts in a STREAMED response's `<head>`. Both boot paths handed the nonce to React — which stamps only the scripts React itself emits — and not to the driver that composes the head, so on the arm a plain `renderMode: 'ssr'` page takes, `renderDeferredRegistryScript()` (executable inline JS) and the `__voltro_state__` payload went out bare under the very `script-src 'nonce-…'` policy the same response set. A deferring page's registry was therefore the one script the browser refused to run. The `renderMode: 'spa'` layout-shell arms were unstamped end to end for the same reason and are fixed with them.
151
+
152
+ `stampScriptNonce` also stops mistaking `data-nonce="…"` or a `?nonce=` query parameter for a nonce attribute, which left exactly the tags a policy then blocked unstamped.
153
+
154
+ Still open, and now documented rather than implied: the settle `<script>` an `<Await>` boundary emits inside the streamed body is part of the rendered tree, so neither stamper reaches it — `defer()` and `cspNonce` do not compose yet.
155
+ - **@voltro/runtime, @voltro/cli, @voltro/plugin-queue, @voltro/plugin-cdc-out** — **A span opened by detached work reached no exporter.** The tracer is a `Layer` provided at `startRpcServer`'s outermost scope, so `Effect.withSpan` inside a handler resolves the configured OpenTelemetry tracer. Work that runs OUTSIDE a request fiber — a broker callback, a schedule firing, a CDC delivery — reaches Effect through its own `runPromise`, which resolves the DEFAULT tracer: it creates spans and hands them to nobody. Measured against a live tracing layer: **0 finished spans**, for three framework span sites that all read as instrumented (`queue.consume`, `cdcOut.deliver`, `plugin.<name>.schedule-fire`).
156
+
157
+ The server now publishes its tracer instance into a process cell (`globalThis` + `Symbol.for`, for the same duplicate-instance reason `coreTablesRegistry` and the row filter are there), and detached work runs under it via `withServerTracer`. That is exactly the correction the LOGGER already had one line over in `rpcServer.ts` — detached fibers "start from the default runtime and carry their own", and were given one; the tracer never was.
158
+
159
+ Not a second provider: a per-plugin tracer means a second `NodeTracerProvider` with an exporter nothing flushes at shutdown, and global OTel registration is refused on purpose. There is one tracer, built where it always was.
160
+
161
+ `withServerTracer` is a no-op when nothing is published — a unit test, an embedder, a process with no rpc server — never a throw and never a second tracer. Pinned by `serverTracer.test.ts`, whose second case drives the defect directly: without the wrapper, the same span never reaches the tracer under test.
162
+ - **@voltro/local-first** — **`useCrdtEditor` was unreachable from a published install.** `@voltro/local-first`'s source `exports` map carried `./editor`, its `publishConfig.exports` — the map an npm install actually resolves — carried only `.` and `./react`, and the build emitted no editor bundle. So the rich-text editor binding worked inside this monorepo (where `workspace:*` resolves the source map) and gave every user `ERR_PACKAGE_PATH_NOT_EXPORTED`, while the docs taught it.
163
+
164
+ `editor` is a build entry now, `./editor` is in the published exports, and it has its own api-extractor report so the surface is checked like the other two. An entry point that is documented and not built is a feature that exists for nobody outside this repo, and the two maps diverging is the shape that hides it: the one you read is not the one users resolve.
165
+ - **@voltro/web** — `responseHeaders` and `cspNonce` are on the type users actually write against.
166
+
167
+ The CSP-nonce and response-header feature shipped with its fields declared in the CLI's internal `MiddlewareResult` and NOT in the one `@voltro/web/middleware` publishes — the type `defineMiddleware` checks a user's `run` against. The runtime honoured both fields; the compiler refused them. So the documented way to set a `Content-Security-Policy` from middleware produced TS2322, and the only way to use a working feature was to cast around its own type.
168
+
169
+ Two definitions of one shape is what allowed it: the half that gained the feature is not the half a user writes against. The published type carries both fields now, with the same documented limits (a prerendered `static` file is served without a render, so no middleware runs; `cspNonce` is `ssr` only, because an isr render is cached and a cached nonce is a lie the browser enforces).
170
+
171
+ Caught by the `web-layout-loader` fixture, which is the only place that writes this type the way a user does.
172
+ - **@voltro/cli** — `voltro mobile <dir>` read its directory with a predicate that treats every flag as valued.
173
+
174
+ The command found its root with a hand-rolled scan — first argument that does not start with `-` and is not preceded by a `--flag`. Two things go wrong with that shape, and `cliPositionalFlagSafety.test.ts` exists because they have gone wrong before:
175
+
176
+ - it treats a BOOLEAN flag as consuming the next argument, so `voltro mobile links --help ./app` decided `./app` was `--help`'s value and silently fell back to `process.cwd()`; - it matched with `indexOf`, the FIRST occurrence, so an argument whose text appears twice was judged by the wrong position.
177
+
178
+ It uses `firstPositional(args, VALUED_FLAGS)` now, with the three flags that actually take a value declared rather than inferred.
179
+ - **@voltro/database** — Adding a `.default(…)` to an EXISTING `text()` column no longer kills the migration on MySQL. MySQL refuses a DEFAULT on a TEXT/BLOB column outright (`BLOB, TEXT, GEOMETRY or JSON column 'x' can't have a default value`), where MariaDB allows it — so the same declaration applied on one engine and failed mid-migration on the other. `CREATE TABLE` has always answered this by widening such a column to `VARCHAR(255)` (`NVARCHAR(450)` on SQL Server, where the reason is indexability); the ALTER path now applies that same answer, reshaping the column to the declared shape instead of setting a default on whatever the column happened to be. A column therefore ends up with the same type whether the default was declared before or after the table existed. Postgres (TEXT takes a DEFAULT) and SQLite (rebuilds to the declared shape) are unchanged. Note the narrowing: on mysql/mariadb the column becomes `VARCHAR(255)`, so the ALTER fails loudly if an existing row is longer — use `text().maxLength(n)` to choose the width. The same widening rule also reached `ADD COLUMN`, which on SQL Server had emitted `NVARCHAR(MAX)` for a column `CREATE TABLE` renders as `NVARCHAR(450)`.
180
+ - **@voltro/cli** — **Validation errors on the no-JavaScript form path rendered in English on every locale.** `validateFields` resolves message ids through a locale whose default is `documentLocale()` — it reads `<html lang>`, and outside a browser that is always `'en'`. The `/form/*` handler runs on the server, so a German page's 422 came back with English field errors while the same form with JavaScript rendered German; the flash carries the resolved strings, so hydration kept them.
113
181
 
114
- The copy fallback (projects where `@voltro/cli` isn't resolvable) merged into an existing `agent-docs/` directory: when a module was renamed upstream (`plugins/versioning.md` `plugins/row-history.md`), a `--force` re-seed brought the new file and left the old one sitting beside it a stale generated doc teaching a package name that no longer exists, which is exactly the claim≠code drift the guide exists to prevent. The mirror is wholly framework-owned, so a re-seed now replaces it (the generator prunes its own output dir the same way).
115
- - **@voltro/cli** — `voltro check`'s observed-diff footer now accounts for every declared procedure, and no result line states a verdict without the count it is a verdict about.
182
+ The handler now resolves the locale from THIS request through the same resolver the surrounding page render and the ISR cache key already use (cookie `Accept-Language` the app's default). It is a required option on `makeFormPostHandler` rather than an optional one: both boot paths mount that handler separately, and an option nobody has to pass is one a new mount silently omits landing straight back on the browser default. An app with no `locales` configured answers `'en'` explicitly.
116
183
 
117
- Two defects, both found by readers doing arithmetic on the output:
184
+ **Under URL-prefix i18n the referring path outranks that chain.** There the language IS the URL (`/de/todos`) and the cookie may say something else entirely, so a cookie-only answer renders errors for a page the user is not on. The handler passes the referer's path to the resolver and both boot paths check it against the app's declared `locales` first, through one shared `localeFromPathPrefix` — a first segment that is not a declared locale (`/design/…`) falls through to the cookie chain rather than being mistaken for one. This is the "referer path vs. locale cookie" question the no-JS form work left open; the answer is both, in that order.
185
+ - **@voltro/client** — `useOutbox`'s `resolveConflict(id, input)` now actually replays the entry it resolved. It replayed against the pre-resolution queue — `replay` reads the queue through a ref, and the `setQueue` beside it had not landed yet — so the entry it was handed was still `conflict`, `replayable()` stopped at it, and nothing was sent. The resolution then sat in the queue as `pending` with no further replay scheduled, and every write queued behind it stayed blocked with it: a conflict that could be resolved in the UI and never left the device.
186
+ - **@voltro/runtime, @voltro/cli** — **A subscription opened over SSE or gRPC ignored the registered row filter — every read, not just the first.** `dispatcher.subscribe` resolves row visibility itself and treats a missing `refilter` as "this app has no row filter", so it read the descriptor unnarrowed. The WebSocket path always supplied one (`bindSubscription`'s `defaultRefilter`); the SSE and gRPC projections go through `makeQuerySubscriber`, whose `subscribeDescriptor` callback passed three arguments and dropped the trailing pair. Both boot paths were affected identically, so nothing a dev/serve parity check looks at could see it.
118
187
 
119
- - The diff computes TWO kinds of blindness — a procedure that never ran, and one that ran with no table access recorded — and the printer named only the first. The printed counts came out short of the total, with no way to tell an unshown category from a defect in `check` itself. The buckets are now derived from the result type, so a third one cannot be added without the label map failing to compile, and a partition that does not close prints as such instead of quietly under-counting. - `no declared/observed mismatches` was a bare verdict sitting under its denominator, and was quotable — and quoted — without it, as a clean bill of health for a surface where almost nothing had been exercised. Every result line now carries its scope (`no declared/observed mismatch among the 12 that ran`), and at zero coverage the section reports the ABSENCE of a comparison (`nothing was compared a declaration is only checked against a procedure that RAN`) rather than the absence of findings. The two are different facts and only one of them is evidence.
120
- - **@voltro/cli** — An `isr` render no longer sees the requesting visitor's credentials — in `voltro dev` and `voltro start` alike.
188
+ The same omission dropped the per-delivery guard re-check, which `data/grpc.md` and the 0.53.0 notes stated as a property: a revoked scope did not end a descriptor-query stream on those two transports. Computed queries were never affected (they re-run their handler, guards included), and neither was any app without a registered row filter.
121
189
 
122
- An isr page's HTML is cached under tenant+locale and served to every visitor inside the revalidate window, but the render itself ran with the FULL request: loaders received the session cookie, and `ctx.query` was bound to it. A loader that read subject-scoped data on an isr page therefore cached the first visitor's data and served it to everyonecache poisoning by construction.
190
+ Three things changed. `makeDefaultRefilter` moved to `@voltro/runtime`'s `rowFilter` module it was private to the WebSocket entrypoint, and the transports that could not reach it are exactly the ones that went unfiltered. `QuerySubscriberDeps.subscribeDescriptor` takes the reauthorizer and the refilter as REQUIRED parameters, so a future transport cannot omit them by writing a shorter callback. And `dispatcher.subscribe` now THROWS, by name, when no refilter is passed while a filter is registered: reading unfiltered on purpose is available, but has to be said out loud with `NO_ROW_FILTER`. That is the same correction `ctx.rowFilter` already carries one layer up`undefined` may mean "this app has none", never "the caller forgot".
123
191
 
124
- The render boundary is fail-closed now: before an isr render runs, the cookie jar (except `voltro:locale`), the `authorization` header and every `x-voltro-*` header are stripped, for the loaders, `ctx.query` AND the `useServerRequest()` snapshot. What survives is exactly what the cache key and locale resolution read: `x-tenant`, `accept-language`, and the locale cookie so a `de` visitor's fill still lands under the `de` key. One shared helper (`isrCredentialStrip.ts`), called by both boot paths, so dev renders isr anonymously exactly as production does — a page can no longer look personalised in dev and silently serve shared HTML in production.
192
+ Pinned by `subscriberTransportRefilter.test.ts`, which drives a real dispatcher through a store that evaluates predicates and asserts the foreign row never crosses the wire in ANY frame, with a positive control so an empty stream cannot pass.
193
+ - **@voltro/cli** — **A template could not ship a font or an image — the scaffolder corrupted every binary file.** `scaffoldFromTemplate` read each file with `readFile(src, 'utf8')` and wrote the string back. That call does not throw on binary input: it substitutes U+FFFD for every undecodable sequence and returns a string, so a woff2 went in at 15 344 bytes and came out at 27 572, silently, in every scaffolded project.
125
194
 
126
- Behavioural consequence, on purpose: a subject-reading loader on an isr page now gets the anonymous answer. A page whose loader needs the signed-in subject belongs on `renderMode: 'ssr'`.
127
- - **@voltro/cli, @voltro/runtime** — **`voltro serve` registers plugin rpc routes again — every plugin-contributed procedure answered `Unknown request tag` in production while `voltro dev` registered all of them.** The serve path mirrored dev's plugin-route block by hand and mirrored exactly half of it: the collision check ran (so nothing warned) and the returned routes were discarded as a bare expression statement, never reaching the buckets the rpc registry is built from. Every plugin route was indistinguishable from a tag that never existed — `useUpload`'s storage tags, presence, every inspect-less plugin rpc — while the app's own procedures answered normally, so the registry looked alive.
195
+ Substitution is now gated on a text-extension allowlist and everything else is copied byte-for-byte. Extension-less files and dotfiles (`Dockerfile`, `.gitignore`, `LICENSE`) count as text they carry tokens, and treating them as binary would ship a literal `{{appName}}`.
128
196
 
129
- Three changes, each aimed at the way this stayed invisible:
197
+ The comment above that line already claimed "we hard-fail if a template author adds one so the failure is visible at boot". There was no such check, and there could not be one on that call. There is now: a file whose extension says text but whose bytes are not valid UTF-8 is REFUSED by name rather than written corrupt, so an allowlist that is wrong about a file fails loudly instead of quietly.
130
198
 
131
- - Both boot paths now call ONE shared builder (`mergePluginRoutesInto`) whose buckets are required parameters — a returned list can be discarded by a statement that typechecks; a function you cannot call without handing it the sinks cannot have its effect dropped. A reachability test drives a real socket with a plugin tag and an invented-tag control, and a source pin keeps the helper pair from being reassembled by hand in either path. - The boot line `plugin routes registered` prints WHENEVER plugins are installed, count included — zero is a finding, and silence is how this shipped. `voltro check` against a running server now also diffs source-declared tags against the live registry (`declared vs live:`), and its offline manifest includes plugin routes, which it previously did not. - Every `Defect` frame the server sends is now also a server log line (`rpc defect sent to client`, ws and http rpc). The defect string used to exist only inside the WebSocket frame — visible in whoever's browser console, invisible to the operator whose server produced it.
199
+ Nothing could see this. `voltro-templates`' own harness classifies files itself and copies non-text ones byte-for-byte, so `pnpm test:templates` was green over a rendering the scaffolder does not perform it validated the harness, not the scaffold a user receives.
@@ -12,12 +12,12 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.97.0",
14
14
  "@effect/rpc": "^0.76.0",
15
- "@voltro/ai": "0.52.0",
16
- "@voltro/cli": "0.52.0",
17
- "@voltro/database": "0.52.0",
18
- "@voltro/env": "0.52.0",
19
- "@voltro/protocol": "0.52.0",
20
- "@voltro/runtime": "0.52.0",
15
+ "@voltro/ai": "0.54.0",
16
+ "@voltro/cli": "0.54.0",
17
+ "@voltro/database": "0.54.0",
18
+ "@voltro/env": "0.54.0",
19
+ "@voltro/protocol": "0.54.0",
20
+ "@voltro/runtime": "0.54.0",
21
21
  "effect": "^3.22.0"
22
22
  },
23
23
  "devDependencies": {
@@ -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.52.0",
17
- "@voltro/database": "0.52.0",
18
- "@voltro/env": "0.52.0",
19
- "@voltro/plugin-auth": "0.52.0",
20
- "@voltro/protocol": "0.52.0",
21
- "@voltro/runtime": "0.52.0",
22
- "@voltro/sql-postgres": "0.52.0",
16
+ "@voltro/cli": "0.54.0",
17
+ "@voltro/database": "0.54.0",
18
+ "@voltro/env": "0.54.0",
19
+ "@voltro/plugin-auth": "0.54.0",
20
+ "@voltro/protocol": "0.54.0",
21
+ "@voltro/runtime": "0.54.0",
22
+ "@voltro/sql-postgres": "0.54.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.52.0",
26
+ "@voltro/testing": "0.54.0",
27
27
  "typescript": "^6.0.3",
28
28
  "@vitest/coverage-v8": "^4.1.10",
29
29
  "vitest": "^4.1.10"
@@ -16,16 +16,16 @@
16
16
  "dependencies": {
17
17
  "@effect/platform": "^0.97.0",
18
18
  "@effect/rpc": "^0.76.0",
19
- "@voltro/cli": "0.52.0",
20
- "@voltro/database": "0.52.0",
21
- "@voltro/env": "0.52.0",
22
- "@voltro/plugin-multitenancy": "0.52.0",
23
- "@voltro/protocol": "0.52.0",
24
- "@voltro/runtime": "0.52.0",
19
+ "@voltro/cli": "0.54.0",
20
+ "@voltro/database": "0.54.0",
21
+ "@voltro/env": "0.54.0",
22
+ "@voltro/plugin-multitenancy": "0.54.0",
23
+ "@voltro/protocol": "0.54.0",
24
+ "@voltro/runtime": "0.54.0",
25
25
  "effect": "^3.22.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@voltro/testing": "0.52.0",
28
+ "@voltro/testing": "0.54.0",
29
29
  "typescript": "^6.0.3",
30
30
  "@vitest/coverage-v8": "^4.1.10",
31
31
  "vitest": "^4.1.10"
@@ -13,16 +13,16 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.52.0",
17
- "@voltro/database": "0.52.0",
18
- "@voltro/env": "0.52.0",
19
- "@voltro/plugin-deactivation": "0.52.0",
20
- "@voltro/protocol": "0.52.0",
21
- "@voltro/runtime": "0.52.0",
16
+ "@voltro/cli": "0.54.0",
17
+ "@voltro/database": "0.54.0",
18
+ "@voltro/env": "0.54.0",
19
+ "@voltro/plugin-deactivation": "0.54.0",
20
+ "@voltro/protocol": "0.54.0",
21
+ "@voltro/runtime": "0.54.0",
22
22
  "effect": "^3.22.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.52.0",
25
+ "@voltro/testing": "0.54.0",
26
26
  "typescript": "^6.0.3",
27
27
  "@vitest/coverage-v8": "^4.1.10",
28
28
  "vitest": "^4.1.10"
@@ -13,18 +13,18 @@
13
13
  "dependencies": {
14
14
  "@react-email/components": "^1.0.12",
15
15
  "@react-email/render": "^1.4.0",
16
- "@voltro/cli": "0.52.0",
17
- "@voltro/database": "0.52.0",
18
- "@voltro/env": "0.52.0",
19
- "@voltro/plugin-mail": "0.52.0",
20
- "@voltro/plugin-multitenancy": "0.52.0",
21
- "@voltro/protocol": "0.52.0",
22
- "@voltro/runtime": "0.52.0",
16
+ "@voltro/cli": "0.54.0",
17
+ "@voltro/database": "0.54.0",
18
+ "@voltro/env": "0.54.0",
19
+ "@voltro/plugin-mail": "0.54.0",
20
+ "@voltro/plugin-multitenancy": "0.54.0",
21
+ "@voltro/protocol": "0.54.0",
22
+ "@voltro/runtime": "0.54.0",
23
23
  "effect": "^3.22.0",
24
24
  "react": "^19.0.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/testing": "0.52.0",
27
+ "@voltro/testing": "0.54.0",
28
28
  "typescript": "^6.0.3",
29
29
  "@vitest/coverage-v8": "^4.1.10",
30
30
  "vitest": "^4.1.10"