@voltro/cli 0.13.0 → 0.15.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 (166) hide show
  1. package/CHANGELOG.md +412 -0
  2. package/dist/{apiBuild-B6_ehbem.js → apiBuild-CHItlwPO.js} +2 -2
  3. package/dist/apiBuild-D8juhYsS.js +2 -0
  4. package/dist/bin.js +3 -3
  5. package/dist/{commands-DYOuGlEC.js → commands-CPQuQRXh.js} +3967 -2831
  6. package/dist/{dbCommand-DIOJQwsq.js → dbCommand-B8F29zV0.js} +2 -1
  7. package/dist/dbCommand-Dt7BtdMd.js +2 -0
  8. package/dist/{dev-xdijqojc.js → dev-BHLM3RPX.js} +1 -1
  9. package/dist/{dev-Fb9BmavX.js → dev-C2lCUhYI.js} +1478 -1347
  10. package/dist/fileConventions-Cu0tM-Bl.js +29 -0
  11. package/dist/{frameworkTableAssembly-CYlrafoK.js → frameworkTableAssembly-BA2bAmYN.js} +164 -163
  12. package/dist/frameworkTableAssembly-D1zJSgOU.js +2 -0
  13. package/dist/index.js +1 -1
  14. package/dist/{inspectMetrics-DOiiII34.js → inspectMetrics-CYDSO3za.js} +955 -945
  15. package/dist/{serveCommand-CqYxzBZR.js → serveCommand-DBzQcCuv.js} +410 -396
  16. package/dist/serveEntry.js +2 -2
  17. package/dist/{start-CjG_0xhR.js → start-BtgatJN3.js} +246 -246
  18. package/dist/startEntry.js +2 -2
  19. package/package.json +17 -17
  20. package/templates/AGENTS.core.md +77 -3
  21. package/templates/AGENTS.md +78 -4
  22. package/templates/agent-docs/_index.md +1 -1
  23. package/templates/agent-docs/_manifest.json +2 -2
  24. package/templates/agent-docs/authentication.md +34 -2
  25. package/templates/agent-docs/cli.md +116 -2
  26. package/templates/agent-docs/data.md +195 -3
  27. package/templates/agent-docs/database/hosting.md +2 -2
  28. package/templates/agent-docs/database/migrations.md +77 -3
  29. package/templates/agent-docs/database/overview.md +11 -0
  30. package/templates/agent-docs/database/seedsdialects.md +53 -1
  31. package/templates/agent-docs/internationalization.md +3 -3
  32. package/templates/agent-docs/introduction.md +133 -2
  33. package/templates/agent-docs/plugins.md +2 -1
  34. package/templates/agent-docs/reference.md +4 -4
  35. package/templates/agent-docs/routing.md +48 -43
  36. package/templates/agent-docs/schema-driven-ui.md +14 -3
  37. package/templates/agent-docs/templates/apibackends.md +1 -8
  38. package/templates/agent-docs/templates/appshells.md +24 -24
  39. package/templates/agent-docs/testing.md +16 -1
  40. package/templates/agent-docs/whats-new.md +247 -39
  41. package/templates/apps/api-ai/database/schema.ts +0 -2
  42. package/templates/apps/api-ai/package.json +7 -7
  43. package/templates/apps/api-auth/package.json +8 -8
  44. package/templates/apps/api-backend/database/schema.ts +0 -3
  45. package/templates/apps/api-backend/package.json +7 -7
  46. package/templates/apps/api-backend-deactivation/database/schema.ts +0 -1
  47. package/templates/apps/api-backend-deactivation/package.json +7 -7
  48. package/templates/apps/api-backend-mail/database/schema.ts +0 -3
  49. package/templates/apps/api-backend-mail/package.json +8 -8
  50. package/templates/apps/api-backend-mariadb/database/schema.ts +0 -3
  51. package/templates/apps/api-backend-mariadb/package.json +9 -9
  52. package/templates/apps/api-backend-storage/database/schema.ts +0 -3
  53. package/templates/apps/api-backend-storage/package.json +8 -8
  54. package/templates/apps/api-data-advanced/database/authors.entity.ts +0 -2
  55. package/templates/apps/api-data-advanced/database/books.entity.ts +0 -1
  56. package/templates/apps/api-data-advanced/package.json +8 -8
  57. package/templates/apps/api-durable/database/schema.ts +0 -3
  58. package/templates/apps/api-durable/package.json +8 -8
  59. package/templates/apps/api-feature-flags/database/schema.ts +0 -1
  60. package/templates/apps/api-feature-flags/package.json +9 -9
  61. package/templates/apps/api-governance/database/schema.ts +0 -1
  62. package/templates/apps/api-governance/package.json +8 -8
  63. package/templates/apps/api-kv/database/schema.ts +0 -3
  64. package/templates/apps/api-kv/package.json +8 -8
  65. package/templates/apps/api-moderation/database/schema.ts +0 -2
  66. package/templates/apps/api-moderation/package.json +8 -8
  67. package/templates/apps/api-observability/database/schema.ts +0 -1
  68. package/templates/apps/api-observability/package.json +8 -8
  69. package/templates/apps/api-ratelimit/database/schema.ts +0 -1
  70. package/templates/apps/api-ratelimit/package.json +8 -8
  71. package/templates/apps/api-rbac/database/schema.ts +0 -2
  72. package/templates/apps/api-rbac/package.json +8 -8
  73. package/templates/apps/api-rest/database/schema.ts +0 -3
  74. package/templates/apps/api-rest/package.json +7 -7
  75. package/templates/apps/api-saas/database/schema.ts +0 -1
  76. package/templates/apps/api-saas/package.json +11 -11
  77. package/templates/apps/api-search/database/schema.ts +0 -1
  78. package/templates/apps/api-search/package.json +8 -8
  79. package/templates/apps/api-versioning/database/schema.ts +0 -1
  80. package/templates/apps/api-versioning/package.json +8 -8
  81. package/templates/apps/api-webhooks/database/schema.ts +0 -1
  82. package/templates/apps/api-webhooks/package.json +9 -9
  83. package/templates/apps/changelog/package.json +6 -6
  84. package/templates/apps/changelog/src/pages/[locale]/{[slug].tsx → [slug].page.tsx} +2 -2
  85. package/templates/apps/changelog/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
  86. package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +2 -2
  87. package/templates/apps/changelog/src/pages/{[slug].test.tsx → [slug].page.test.tsx} +1 -1
  88. package/templates/apps/changelog/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
  89. package/templates/apps/edge-functions/package.json +2 -2
  90. package/templates/apps/frontend-admin/package.json +8 -8
  91. package/templates/apps/frontend-admin/src/pages/(marketing)/{index.test.tsx → index.page.test.tsx} +1 -1
  92. package/templates/apps/frontend-admin/src/pages/(marketing)/{login.test.tsx → login.page.test.tsx} +1 -1
  93. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +1 -1
  94. package/templates/apps/frontend-admin/src/pages/admin/{index.test.tsx → index.page.test.tsx} +1 -1
  95. package/templates/apps/frontend-app/package.json +8 -8
  96. package/templates/apps/frontend-app/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
  97. package/templates/apps/frontend-app/src/pages/{schema-ui.test.tsx → schema-ui.page.test.tsx} +1 -1
  98. package/templates/apps/frontend-blank/package.json +7 -7
  99. package/templates/apps/frontend-blank/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
  100. package/templates/apps/frontend-contact/package.json +7 -7
  101. package/templates/apps/frontend-contact/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
  102. package/templates/apps/frontend-contact/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
  103. package/templates/apps/frontend-dashboard/package.json +7 -7
  104. package/templates/apps/frontend-dashboard/src/pages/(marketing)/{index.test.tsx → index.page.test.tsx} +1 -1
  105. package/templates/apps/frontend-dashboard/src/pages/(marketing)/{login.test.tsx → login.page.test.tsx} +1 -1
  106. package/templates/apps/frontend-dashboard/src/pages/dashboard/{index.test.tsx → index.page.test.tsx} +1 -1
  107. package/templates/apps/frontend-dashboard/src/pages/dashboard/{settings.test.tsx → settings.page.test.tsx} +1 -1
  108. package/templates/apps/frontend-docs/package.json +7 -7
  109. package/templates/apps/frontend-docs/src/pages/[locale]/docs/{[...slug].tsx → [...slug].page.tsx} +3 -3
  110. package/templates/apps/frontend-docs/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
  111. package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +2 -2
  112. package/templates/apps/frontend-docs/src/pages/docs/{[...slug].test.tsx → [...slug].page.test.tsx} +1 -1
  113. package/templates/apps/frontend-docs/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
  114. package/templates/apps/frontend-i18n/package.json +6 -6
  115. package/templates/apps/frontend-i18n/src/pages/[locale]/{about.tsx → about.page.tsx} +2 -2
  116. package/templates/apps/frontend-i18n/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
  117. package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +2 -2
  118. package/templates/apps/frontend-i18n/src/pages/{about.test.tsx → about.page.test.tsx} +1 -1
  119. package/templates/apps/frontend-i18n/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
  120. package/templates/apps/frontend-landing/package.json +7 -7
  121. package/templates/apps/frontend-landing/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
  122. package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +1 -1
  123. package/templates/apps/frontend-landing/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
  124. package/templates/apps/frontend-spa/package.json +7 -7
  125. package/templates/apps/frontend-spa/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
  126. package/templates/apps/frontend-ssr/package.json +7 -7
  127. package/templates/apps/frontend-ssr/src/pages/{feed-swr.test.tsx → feed-swr.page.test.tsx} +1 -1
  128. package/templates/apps/frontend-ssr/src/pages/{feed.test.tsx → feed.page.test.tsx} +1 -1
  129. package/templates/apps/frontend-ssr/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
  130. package/templates/apps/frontend-ssr-api/package.json +8 -8
  131. package/templates/apps/frontend-ssr-api/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
  132. package/templates/apps/frontend-static-blog/package.json +6 -6
  133. package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/{[slug].tsx → [slug].page.tsx} +2 -2
  134. package/templates/apps/frontend-static-blog/src/pages/[locale]/{index.tsx → index.page.tsx} +2 -2
  135. package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +2 -2
  136. package/templates/apps/frontend-static-blog/src/pages/blog/{[slug].test.tsx → [slug].page.test.tsx} +1 -1
  137. package/templates/apps/frontend-static-blog/src/pages/{index.test.tsx → index.page.test.tsx} +1 -1
  138. package/dist/apiBuild-Bzpb383i.js +0 -2
  139. package/dist/dbCommand-ADD4mrTI.js +0 -2
  140. package/dist/frameworkTableAssembly-XvKO7wQt.js +0 -2
  141. /package/templates/apps/changelog/src/pages/{[slug].tsx → [slug].page.tsx} +0 -0
  142. /package/templates/apps/changelog/src/pages/{index.tsx → index.page.tsx} +0 -0
  143. /package/templates/apps/frontend-admin/src/pages/(marketing)/{index.tsx → index.page.tsx} +0 -0
  144. /package/templates/apps/frontend-admin/src/pages/(marketing)/{login.tsx → login.page.tsx} +0 -0
  145. /package/templates/apps/frontend-admin/src/pages/admin/{[entity].tsx → [entity].page.tsx} +0 -0
  146. /package/templates/apps/frontend-admin/src/pages/admin/{index.tsx → index.page.tsx} +0 -0
  147. /package/templates/apps/frontend-app/src/pages/{index.tsx → index.page.tsx} +0 -0
  148. /package/templates/apps/frontend-app/src/pages/{schema-ui.tsx → schema-ui.page.tsx} +0 -0
  149. /package/templates/apps/frontend-blank/src/pages/{index.tsx → index.page.tsx} +0 -0
  150. /package/templates/apps/frontend-contact/src/pages/{index.tsx → index.page.tsx} +0 -0
  151. /package/templates/apps/frontend-dashboard/src/pages/(marketing)/{index.tsx → index.page.tsx} +0 -0
  152. /package/templates/apps/frontend-dashboard/src/pages/(marketing)/{login.tsx → login.page.tsx} +0 -0
  153. /package/templates/apps/frontend-dashboard/src/pages/dashboard/{index.tsx → index.page.tsx} +0 -0
  154. /package/templates/apps/frontend-dashboard/src/pages/dashboard/{settings.tsx → settings.page.tsx} +0 -0
  155. /package/templates/apps/frontend-docs/src/pages/docs/{[...slug].tsx → [...slug].page.tsx} +0 -0
  156. /package/templates/apps/frontend-docs/src/pages/{index.tsx → index.page.tsx} +0 -0
  157. /package/templates/apps/frontend-i18n/src/pages/{about.tsx → about.page.tsx} +0 -0
  158. /package/templates/apps/frontend-i18n/src/pages/{index.tsx → index.page.tsx} +0 -0
  159. /package/templates/apps/frontend-landing/src/pages/{index.tsx → index.page.tsx} +0 -0
  160. /package/templates/apps/frontend-spa/src/pages/{index.tsx → index.page.tsx} +0 -0
  161. /package/templates/apps/frontend-ssr/src/pages/{feed-swr.tsx → feed-swr.page.tsx} +0 -0
  162. /package/templates/apps/frontend-ssr/src/pages/{feed.tsx → feed.page.tsx} +0 -0
  163. /package/templates/apps/frontend-ssr/src/pages/{index.tsx → index.page.tsx} +0 -0
  164. /package/templates/apps/frontend-ssr-api/src/pages/{index.tsx → index.page.tsx} +0 -0
  165. /package/templates/apps/frontend-static-blog/src/pages/blog/{[slug].tsx → [slug].page.tsx} +0 -0
  166. /package/templates/apps/frontend-static-blog/src/pages/{index.tsx → index.page.tsx} +0 -0
@@ -1,3 +1,3 @@
1
- import { X as e } from "./inspectMetrics-DOiiII34.js";
2
- import { t } from "./start-CjG_0xhR.js";
1
+ import { $ as e } from "./inspectMetrics-CYDSO3za.js";
2
+ import { t } from "./start-BtgatJN3.js";
3
3
  export { e as loadDotEnv, t as runStartCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -62,22 +62,22 @@
62
62
  "@effect/platform-node": "^0.107.0",
63
63
  "@effect/sql": "^0.51.1",
64
64
  "@effect/workflow": "^0.18.2",
65
- "@voltro/ai": "0.13.0",
66
- "@voltro/cache": "0.13.0",
67
- "@voltro/data-transfer": "0.13.0",
68
- "@voltro/database": "0.13.0",
69
- "@voltro/env": "0.13.0",
70
- "@voltro/kv": "0.13.0",
71
- "@voltro/logger": "0.13.0",
72
- "@voltro/plugin-auth": "0.13.0",
73
- "@voltro/plugin-broadcast": "0.13.0",
74
- "@voltro/plugin-mail": "0.13.0",
75
- "@voltro/plugin-storage": "0.13.0",
76
- "@voltro/plugin-webhooks": "0.13.0",
77
- "@voltro/protocol": "0.13.0",
78
- "@voltro/runtime": "0.13.0",
79
- "@voltro/serverless": "0.13.0",
80
- "@voltro/workflow": "0.13.0",
65
+ "@voltro/ai": "0.15.0",
66
+ "@voltro/cache": "0.15.0",
67
+ "@voltro/data-transfer": "0.15.0",
68
+ "@voltro/database": "0.15.0",
69
+ "@voltro/env": "0.15.0",
70
+ "@voltro/kv": "0.15.0",
71
+ "@voltro/logger": "0.15.0",
72
+ "@voltro/plugin-auth": "0.15.0",
73
+ "@voltro/plugin-broadcast": "0.15.0",
74
+ "@voltro/plugin-mail": "0.15.0",
75
+ "@voltro/plugin-storage": "0.15.0",
76
+ "@voltro/plugin-webhooks": "0.15.0",
77
+ "@voltro/protocol": "0.15.0",
78
+ "@voltro/runtime": "0.15.0",
79
+ "@voltro/serverless": "0.15.0",
80
+ "@voltro/workflow": "0.15.0",
81
81
  "chokidar": "^5.0.0",
82
82
  "ioredis": "^5.11.1",
83
83
  "tinyglobby": "^0.2.17",
@@ -219,7 +219,7 @@ a `web/` (frontend).
219
219
  │ │ └── rpcGroup.generated.ts # codegen — do NOT edit
220
220
  │ └── web/
221
221
  │ ├── app.config.ts # { type:'web', apis, theme, locales, … }
222
- │ └── src/pages/**/*.tsx # file-based routing
222
+ │ └── src/pages/**/*.page.tsx # file-based routing (only *.page.tsx)
223
223
  ├── packages/ # shared libs
224
224
  └── AGENTS.md / CLAUDE.md # this file
225
225
  ```
@@ -249,7 +249,77 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
249
249
  | `*.agent.tsx` + `*.agent.server.tsx` | server-side LLM chat |
250
250
  | `*.tool.tsx` | tool an agent can call |
251
251
  | `*.entity.ts` / `*.schema.ts` / `schema.ts` | one table per file |
252
- | `src/pages/**/*.tsx` | web page (file-based route) |
252
+ | `*.page.tsx` (under `src/pages/`) | web page (file-based route) + `*.page.test.tsx` |
253
+ | `*.component.tsx` | exactly ONE component (+ types) |
254
+ | `*.component.ui.tsx` | presentational: one component, READS only — never writes |
255
+ | `*.hook.ts` | exactly ONE `use*` hook (+ types) |
256
+ | `*.types.ts` | types only — zero runtime exports |
257
+ | `*.internal.ts` | only its own directory subtree may import it |
258
+ | `*.fixture.ts` | test material — no production path may reach it |
259
+ | `*.tracking.ts` | the ONLY place analytics may be called from |
260
+ | `*.store.ts` | exactly ONE `defineStore` — client state, never server state |
261
+ | `*.client.ts` | declares itself + its transitive imports browser-safe |
262
+
263
+ **Only `*.page.tsx` is a route.** Everything else under `src/pages/` — components,
264
+ hooks, tests — is ordinary code with no URL, so put it next to the page that uses
265
+ it rather than in a distant `components/` directory. The suffix decides *whether*
266
+ a file is a route; the path under `src/pages/` decides *which URL*. A `*.page.tsx`
267
+ outside `src/pages/` never routes. A page MUST default-export its component —
268
+ without it `voltro dev` refuses to boot and names the file.
269
+
270
+ **Every suffix above is a CONTRACT, not a label.** Something else depends on each
271
+ promise, and `voltro doctor` enforces all of them:
272
+
273
+ - `*.component.ui.tsx` may read (`useT`, `useCan`, `usePermissions`) but must
274
+ never import a write hook (`useMutation`, `useAction`, `useUpload`, …). That is
275
+ what lets a caller render it ten thousand times in a list, reuse it across
276
+ features, and prerender it without reading its source. Lift the write into the
277
+ owning `*.component.tsx` and pass a handler down.
278
+ - `*.internal.ts` is the promise that refactoring inside that directory breaks
279
+ nobody. An import from another subtree revokes it.
280
+ - `*.types.ts` having no runtime export is what makes importing it free and makes
281
+ it impossible for it to sit in a runtime import cycle.
282
+ - `*.store.ts` holds ONE `defineStore`. Reads go through a selector
283
+ (`wizard.use((s) => s.step)`) — there is no whole-state read, because a
284
+ component holding all of it re-renders on every field. Scope a per-entity
285
+ instance with a KEY (`{ key: orderId }`), never a Provider. Seed it for SSR
286
+ from a loader with `seedStore(...)`; it rides the hydration payload the router
287
+ already writes. An action that writes SEVERAL fields wraps them in
288
+ `store.batch('label', () => …)` — one notification, one devtools entry, one
289
+ undo step, and a rollback if it throws; `await` first, then batch (an async
290
+ callback is an error). `{ persist: { key } }` survives a reload.
291
+ NEVER put server data in a store: a subscription is already
292
+ live, and a copy is not — the page then shows the stale one.
293
+ - `*.tracking.ts` is where every `defineTracking(...)` spec lives — the event
294
+ catalogue, so "what do we send to third parties" is a file listing rather than
295
+ archaeology. A component wires one up with `useTracking(spec, props, sink)`; it
296
+ NAMES a spec, it never declares one. (`useTracking` is a hook, so it is not
297
+ confined — only the declaration is.)
298
+ - Do NOT rename a file to `*.component.ui.tsx` just because it happens not to
299
+ write today. The suffix is a promise about what it MAY do.
300
+
301
+ **These rules apply to code YOU write — never to vendored code.** A shadcn
302
+ component (`npx shadcn add`) follows shadcn's conventions and is overwritten by
303
+ the next `add`; renaming it breaks their convention and is undone next run. A
304
+ directory is exempt when the app's `components.json` names it (`aliases.ui`) or
305
+ it carries a `.voltro-vendored` file whose first line says where the code came
306
+ from. Never add that marker to a directory of your own code — the point of the
307
+ rules is that everything we author follows them.
308
+
309
+ **Write the test in the same step as the code — always, for every primitive.**
310
+ Not "afterwards", not in a cleanup pass: the filename is derivable
311
+ (`users/[id].page.tsx` → `users/[id].page.test.tsx`,
312
+ `Card.component.tsx` → `Card.component.test.tsx`,
313
+ `order.mutation.server.ts` → `order.mutation.server.test.ts`), so there is
314
+ nothing to decide. A change is not finished until its test exists and passes.
315
+
316
+ Two traps worth naming, because a green run can hide both:
317
+
318
+ - **`voltro test` transpiles without type-checking.** A type error in a TEST file
319
+ passes the suite and fails the package's `typecheck`. Run both.
320
+ - **Never fake a service to make a test pass.** A test that only re-asserts its
321
+ own mock's return value is worse than no test — it reports coverage it does not
322
+ have. Assert the descriptor shape instead and cover the real path elsewhere.
253
323
 
254
324
  ## app.config.ts
255
325
 
@@ -344,9 +414,13 @@ declare it). Mixins compose via `.with(...)` and auto-stamp bookkeeping columns:
344
414
  ```ts
345
415
  export const todos = table('todos', { id: id(), title: text(), done: boolean().default(false) })
346
416
  .with(softDelete(), tenant()) // tenant() transitively requires audit()
347
- .reactive() // opt into the matcher engine (subscriptions)
348
417
  ```
349
418
 
419
+ - Every table is REACTIVE by default — nothing to opt into. `.nonReactive()`
420
+ turns reactivity OFF for a table entirely: no subscriber fires, locally or
421
+ across instances (the write still happens). Use it for a hot table nobody
422
+ subscribes to; never for one a query reads — that subscription never fires,
423
+ and `voltro dev` warns.
350
424
  - `id()` defaults to a branded **TypeID** (`todo_…`). Insert auto-injects it.
351
425
  - Indexes are **table-level** (`.index('name', [...])`) — there is NO
352
426
  column-level `.index()`. Declare `.index([...])` AFTER `.with(...)` to address
@@ -219,7 +219,7 @@ a `web/` (frontend).
219
219
  │ │ └── rpcGroup.generated.ts # codegen — do NOT edit
220
220
  │ └── web/
221
221
  │ ├── app.config.ts # { type:'web', apis, theme, locales, … }
222
- │ └── src/pages/**/*.tsx # file-based routing
222
+ │ └── src/pages/**/*.page.tsx # file-based routing (only *.page.tsx)
223
223
  ├── packages/ # shared libs
224
224
  └── AGENTS.md / CLAUDE.md # this file
225
225
  ```
@@ -249,7 +249,77 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
249
249
  | `*.agent.tsx` + `*.agent.server.tsx` | server-side LLM chat |
250
250
  | `*.tool.tsx` | tool an agent can call |
251
251
  | `*.entity.ts` / `*.schema.ts` / `schema.ts` | one table per file |
252
- | `src/pages/**/*.tsx` | web page (file-based route) |
252
+ | `*.page.tsx` (under `src/pages/`) | web page (file-based route) + `*.page.test.tsx` |
253
+ | `*.component.tsx` | exactly ONE component (+ types) |
254
+ | `*.component.ui.tsx` | presentational: one component, READS only — never writes |
255
+ | `*.hook.ts` | exactly ONE `use*` hook (+ types) |
256
+ | `*.types.ts` | types only — zero runtime exports |
257
+ | `*.internal.ts` | only its own directory subtree may import it |
258
+ | `*.fixture.ts` | test material — no production path may reach it |
259
+ | `*.tracking.ts` | the ONLY place analytics may be called from |
260
+ | `*.store.ts` | exactly ONE `defineStore` — client state, never server state |
261
+ | `*.client.ts` | declares itself + its transitive imports browser-safe |
262
+
263
+ **Only `*.page.tsx` is a route.** Everything else under `src/pages/` — components,
264
+ hooks, tests — is ordinary code with no URL, so put it next to the page that uses
265
+ it rather than in a distant `components/` directory. The suffix decides *whether*
266
+ a file is a route; the path under `src/pages/` decides *which URL*. A `*.page.tsx`
267
+ outside `src/pages/` never routes. A page MUST default-export its component —
268
+ without it `voltro dev` refuses to boot and names the file.
269
+
270
+ **Every suffix above is a CONTRACT, not a label.** Something else depends on each
271
+ promise, and `voltro doctor` enforces all of them:
272
+
273
+ - `*.component.ui.tsx` may read (`useT`, `useCan`, `usePermissions`) but must
274
+ never import a write hook (`useMutation`, `useAction`, `useUpload`, …). That is
275
+ what lets a caller render it ten thousand times in a list, reuse it across
276
+ features, and prerender it without reading its source. Lift the write into the
277
+ owning `*.component.tsx` and pass a handler down.
278
+ - `*.internal.ts` is the promise that refactoring inside that directory breaks
279
+ nobody. An import from another subtree revokes it.
280
+ - `*.types.ts` having no runtime export is what makes importing it free and makes
281
+ it impossible for it to sit in a runtime import cycle.
282
+ - `*.store.ts` holds ONE `defineStore`. Reads go through a selector
283
+ (`wizard.use((s) => s.step)`) — there is no whole-state read, because a
284
+ component holding all of it re-renders on every field. Scope a per-entity
285
+ instance with a KEY (`{ key: orderId }`), never a Provider. Seed it for SSR
286
+ from a loader with `seedStore(...)`; it rides the hydration payload the router
287
+ already writes. An action that writes SEVERAL fields wraps them in
288
+ `store.batch('label', () => …)` — one notification, one devtools entry, one
289
+ undo step, and a rollback if it throws; `await` first, then batch (an async
290
+ callback is an error). `{ persist: { key } }` survives a reload.
291
+ NEVER put server data in a store: a subscription is already
292
+ live, and a copy is not — the page then shows the stale one.
293
+ - `*.tracking.ts` is where every `defineTracking(...)` spec lives — the event
294
+ catalogue, so "what do we send to third parties" is a file listing rather than
295
+ archaeology. A component wires one up with `useTracking(spec, props, sink)`; it
296
+ NAMES a spec, it never declares one. (`useTracking` is a hook, so it is not
297
+ confined — only the declaration is.)
298
+ - Do NOT rename a file to `*.component.ui.tsx` just because it happens not to
299
+ write today. The suffix is a promise about what it MAY do.
300
+
301
+ **These rules apply to code YOU write — never to vendored code.** A shadcn
302
+ component (`npx shadcn add`) follows shadcn's conventions and is overwritten by
303
+ the next `add`; renaming it breaks their convention and is undone next run. A
304
+ directory is exempt when the app's `components.json` names it (`aliases.ui`) or
305
+ it carries a `.voltro-vendored` file whose first line says where the code came
306
+ from. Never add that marker to a directory of your own code — the point of the
307
+ rules is that everything we author follows them.
308
+
309
+ **Write the test in the same step as the code — always, for every primitive.**
310
+ Not "afterwards", not in a cleanup pass: the filename is derivable
311
+ (`users/[id].page.tsx` → `users/[id].page.test.tsx`,
312
+ `Card.component.tsx` → `Card.component.test.tsx`,
313
+ `order.mutation.server.ts` → `order.mutation.server.test.ts`), so there is
314
+ nothing to decide. A change is not finished until its test exists and passes.
315
+
316
+ Two traps worth naming, because a green run can hide both:
317
+
318
+ - **`voltro test` transpiles without type-checking.** A type error in a TEST file
319
+ passes the suite and fails the package's `typecheck`. Run both.
320
+ - **Never fake a service to make a test pass.** A test that only re-asserts its
321
+ own mock's return value is worse than no test — it reports coverage it does not
322
+ have. Assert the descriptor shape instead and cover the real path elsewhere.
253
323
 
254
324
  ## app.config.ts
255
325
 
@@ -344,9 +414,13 @@ declare it). Mixins compose via `.with(...)` and auto-stamp bookkeeping columns:
344
414
  ```ts
345
415
  export const todos = table('todos', { id: id(), title: text(), done: boolean().default(false) })
346
416
  .with(softDelete(), tenant()) // tenant() transitively requires audit()
347
- .reactive() // opt into the matcher engine (subscriptions)
348
417
  ```
349
418
 
419
+ - Every table is REACTIVE by default — nothing to opt into. `.nonReactive()`
420
+ turns reactivity OFF for a table entirely: no subscriber fires, locally or
421
+ across instances (the write still happens). Use it for a hot table nobody
422
+ subscribes to; never for one a query reads — that subscription never fires,
423
+ and `voltro dev` warns.
350
424
  - `id()` defaults to a branded **TypeID** (`todo_…`). Insert auto-injects it.
351
425
  - Indexes are **table-level** (`.index('name', [...])`) — there is NO
352
426
  column-level `.index()`. Declare `.index([...])` AFTER `.with(...)` to address
@@ -452,7 +526,7 @@ each plugin's own README.
452
526
 
453
527
  | Topic | Open | Summary |
454
528
  |---|---|---|
455
- | **What's new in 0.13.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
529
+ | **What's new in 0.14.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
456
530
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
457
531
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
458
532
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -9,7 +9,7 @@ each plugin's own README.
9
9
 
10
10
  | Topic | Open | Summary |
11
11
  |---|---|---|
12
- | **What's new in 0.13.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
12
+ | **What's new in 0.14.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
13
13
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
14
14
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
15
15
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -53,7 +53,7 @@
53
53
  "group": null,
54
54
  "description": "How Voltro's reactive data layer works — queries, mutations, actions, streams, all over one WebSocket with typed errors and tracked dependencies.",
55
55
  "path": "agent-docs/data.md",
56
- "files": 16
56
+ "files": 17
57
57
  },
58
58
  {
59
59
  "id": "database/advancedqueries",
@@ -179,7 +179,7 @@
179
179
  "group": null,
180
180
  "description": "Scaffold a Voltro project and boot it locally in under a minute.",
181
181
  "path": "agent-docs/introduction.md",
182
- "files": 5
182
+ "files": 6
183
183
  },
184
184
  {
185
185
  "id": "multi-tenancy",
@@ -525,6 +525,7 @@ interface AuthStrategy {
525
525
  interface AuthStrategyInput {
526
526
  readonly headers: Readonly<Record<string, string | undefined>>
527
527
  readonly clientId: number // per-connection id (for soft re-auth)
528
+ readonly store?: DataStore // the app's store, for a DB-backed strategy
528
529
  }
529
530
  ```
530
531
 
@@ -540,6 +541,34 @@ The `failed`-stops-the-chain rule is a **security** decision, not an ergonomic o
540
541
 
541
542
  > Strategies must be **fast on the no-match path** — a cookie-name substring check, no IO — because every strategy runs on every request until one matches. Do JWKS fetches / DB lookups only *after* you've confirmed the request is yours, and cache them.
542
543
 
544
+ ### Reading your database — `input.store`
545
+
546
+ A strategy that identifies the caller from a row — a session, an API key, a PAT
547
+ — gets the app's `DataStore` on its input:
548
+
549
+ ```ts
550
+ const dbSession: AuthStrategy = {
551
+ id: 'db-session',
552
+ resolve: async ({ headers, store }) => {
553
+ const token = headers.authorization?.slice('Bearer '.length)
554
+ if (token === undefined) return { kind: 'skip' }
555
+ if (store === undefined) return { kind: 'skip' }
556
+ const [row] = await store.query(sessions.byToken(token))
557
+ return row ? { kind: 'matched', subject: toSubject(row) } : { kind: 'failed', reason: 'unknown token' }
558
+ },
559
+ }
560
+ ```
561
+
562
+ It is the **boot** store, not a request-scoped one — strategies resolve before a
563
+ request store exists — and the same value `auth.resolveScopes` receives.
564
+ `undefined` only while the store is still being built (`voltro dev` builds it
565
+ after the auth chain) and on an app with no store, so a strategy should `skip`
566
+ rather than throw.
567
+
568
+ Read users, sessions, keys. A strategy that runs domain writes while deciding
569
+ who the caller is has the two jobs the wrong way round; nothing in the type
570
+ stops you, and it is still wrong.
571
+
543
572
  ## Composing the chain
544
573
 
545
574
  `composeAuthStrategies` turns an ordered list of strategies into a single resolver. First `matched` wins; first `failed` short-circuits to anonymous.
@@ -573,8 +602,9 @@ If your authorization is a database ROLE rather than a scope on the token, the f
573
602
  // app.config.ts
574
603
  export default defineApiConfig({
575
604
  auth: {
576
- resolveScopes: async (subject) => {
577
- const role = await roleCache.get(subject.id) // cache itsee below
605
+ resolveScopes: async (subject, { store }) => {
606
+ if (store === undefined) return [] // still bootingclaim nothing
607
+ const role = await roleCache.get(subject.id, store) // cache it — see below
578
608
  return role === 'admin' ? ['employee:admin', 'employee:read'] : ['employee:read']
579
609
  },
580
610
  },
@@ -591,6 +621,8 @@ export const payrollList = defineQuery({
591
621
  })
592
622
  ```
593
623
 
624
+ **You get the app's DataStore.** A role lives in the database, and without it the only way to reach one was a second connection path beside the framework's — to the same database the request store opens a moment later. It is the BOOT store, not a request-scoped one: strategies resolve before a request store exists, so it is `undefined` while the store is still being built. Return `[]` then rather than guessing.
625
+
594
626
  **Scopes only — never a Subject.** The hook cannot change `id` or `tenantId`: identity belongs to the auth strategy, and a hook that could rewrite it would be a forgery surface. The returned scopes are UNIONED with whatever the strategy already set, so a resolver can grant authority but never revoke it.
595
627
 
596
628
  **It does not run for anonymous callers** — there is no identity to look a role up for.
@@ -154,6 +154,20 @@ requiring a scope no role grants, which makes that procedure permanently and
154
154
  silently uncallable. (With a custom `resolvePermissions` the vocabulary isn't
155
155
  exhaustive, so the rule stays quiet rather than flagging correct code.)
156
156
 
157
+ `rbac/unenforced-scope` is the same registry read in the other direction: a
158
+ scope a role GRANTS that no handler ever guards on. That direction has no
159
+ artefact to inspect — you cannot grep for an authorization check that was never
160
+ written, which is exactly why it survives review. One app modelled
161
+ `api-keys:write` in its role catalogue, complete and reviewed, and no handler
162
+ checked it: any member could mint a shared credential, and nothing failed.
163
+ Tests pass when an authorization check is missing.
164
+
165
+ It is a **warning**, because three innocent explanations exist: a plugin route
166
+ enforces it internally (the graph cannot see inside a plugin), a REST route
167
+ carries its own guards, or it is a UI-affordance scope that `useCan` reads to
168
+ hide a button and no server check backs on purpose. All three are fine. Not
169
+ knowing which is not.
170
+
157
171
  #### Declared vs OBSERVED — reconciled against reality
158
172
 
159
173
  A query's `source` and a mutation's `targets` are not documentation: the
@@ -466,6 +480,26 @@ The cleaner fix is on your side and worth doing anyway: start the container as
466
480
  the workspace owner, `docker run --user $(id -u):$(id -g)`. Then nothing needs
467
481
  handing over at all.
468
482
 
483
+ ### What the boot tells you
484
+
485
+ Three checks run at boot and print one line each when they have something to say
486
+ — never fatal, and silent when the answer is fine:
487
+
488
+ - **A reactive table with no change trigger** (postgres). The schema fingerprint
489
+ covers columns, not triggers, so a restored dump or a hand-run `DROP TRIGGER`
490
+ leaves the schema "up to date" and the subscription silently not reaching other
491
+ instances. The reverse is reported too: a `.nonReactive()` table still carrying
492
+ a trigger pays `REPLICA IDENTITY FULL` and a NOTIFY per write for nothing.
493
+ - **A `.nonReactive()` table that a query reads.** That subscription will never
494
+ fire — first snapshot, then silence forever.
495
+ - **`apiKeys: true` with no `apikeys:issue:*` scope declared.** The capability is
496
+ on and reachable by nobody; every issue request fails its guard.
497
+
498
+ `voltro doctor` adds a fourth, over your source: an executor that writes
499
+ `ctx.request.subject.id` while naming no guard, which is how an anonymous caller
500
+ reaches a NOT NULL column and gets a raw statement failure instead of a typed
501
+ refusal.
502
+
469
503
  ## `voltro dev <appDir>`
470
504
 
471
505
  ```bash
@@ -684,6 +718,45 @@ The restart is a full re-exec — there is no in-process hot-reload of a
684
718
  handler body; editing a query's executor respawns the child (debounced
685
719
  80ms, so a burst of saves collapses into one restart).
686
720
 
721
+ ## When the dev server stops
722
+
723
+ A restart replaces the child; the supervisor keeps watching. When the dev
724
+ server exits **on its own** — an aborted boot, or you stopping it — what
725
+ happens next depends on whether anyone is there to react.
726
+
727
+ **In a terminal**, a crashed boot is something you are about to fix, so
728
+ the supervisor keeps watching and tells you:
729
+
730
+ ```text
731
+ dev server crashed — waiting for a file change exitCode=1
732
+ ```
733
+
734
+ Fix the cause and save. The watcher restarts the server exactly as it
735
+ would for any other edit; you don't retype the command.
736
+
737
+ **Piped, backgrounded or in CI** — anywhere stdout is not a TTY — nobody
738
+ is going to fix anything, so `voltro dev` exits with the child's code:
739
+
740
+ ```text
741
+ dev server exited — supervisor stopping exitCode=1
742
+ ```
743
+
744
+ That half matters because the supervisor used to keep watching in *both*
745
+ cases: a shell that had long since closed still had a `voltro dev` behind
746
+ it holding a watcher, and a CI job that had "finished" kept its runner
747
+ busy. A failed boot is now a failed command wherever no one is looking.
748
+
749
+ A **clean** exit always stops the supervisor, watched or not — a dev
750
+ server ending on purpose is not something to wait out.
751
+
752
+ | Env | Effect |
753
+ |---|---|
754
+ | `VOLTRO_DEV_KEEP_ALIVE=1` | Wait for a fix even without a TTY — a CI runner with a TTY allocated, or a wrapper that pipes output while you watch it. |
755
+ | `VOLTRO_DEV_KEEP_ALIVE=0` | Exit on a crash even in a terminal. |
756
+
757
+ Neither can keep a clean exit alive; that would turn a deliberate
758
+ shutdown into a hang.
759
+
687
760
  The `.env` trigger applies to **both api and web** apps — `process.env` is
688
761
  parsed once at boot, so editing a `.env` (or re-pulling secrets, e.g.
689
762
  `doppler secrets download > .env`) needs a full re-exec. `voltro dev` watches
@@ -1138,8 +1211,8 @@ A page is listed when ALL of these hold:
1138
1211
 
1139
1212
  ```text
1140
1213
  • renderMode:'spa' candidates (2 pages — loader-free, under a layout, currently ssr/default):
1141
- src/pages/dashboard/index.tsx (/dashboard) — default renderMode
1142
- src/pages/admin/settings.tsx (/admin/settings) — renderMode:'ssr'
1214
+ src/pages/dashboard/index.page.tsx (/dashboard) — default renderMode
1215
+ src/pages/admin/settings.page.tsx (/admin/settings) — renderMode:'ssr'
1143
1216
  → renderMode:'spa' skips this page's SSR compile while its layout shell still renders server-side — adopt it if the page BODY does not need SSR (internal/authenticated pages); keep 'ssr' if the page content needs SEO or server first-paint.
1144
1217
  ```
1145
1218
 
@@ -2221,6 +2294,7 @@ voltro update # bump to the latest published version, install, r
2221
2294
  voltro update --to 0.4.0 # pin an explicit target version
2222
2295
  voltro update --dry-run # preview the bump + which codemods would run — writes nothing
2223
2296
  voltro update --force # allow a dirty working tree (not recommended)
2297
+ voltro update --only 0.14.0/03_pages-suffix # run just these codemod(s), comma-separated
2224
2298
  voltro update --exact # pin exact versions (drop the ^ / ~ range prefix)
2225
2299
  voltro update --help # every flag — always answered, even on a dirty tree
2226
2300
 
@@ -2229,6 +2303,26 @@ voltro update --codemods-only --from 0.3.0 # re-run codemods 0.3.0
2229
2303
  voltro update --codemods-only --from 0.3.0 --to 0.4.0 # explicit delta
2230
2304
  ```
2231
2305
 
2306
+ ### Taking only part of the jump — `--only`
2307
+
2308
+ Ids are what `--dry-run` prints:
2309
+
2310
+ ```bash
2311
+ voltro update --dry-run
2312
+ voltro update --codemods-only --from 0.13.0 --only 0.14.0/03_pages-suffix,0.14.0/02_reactive-by-default
2313
+ ```
2314
+
2315
+ Useful when part of a jump is load-bearing (without it the app does not build or
2316
+ its routes 404) and part is elective: take the necessary ones, get back to a
2317
+ committable tree, then run the rest. An id that matches nothing in the jump is an
2318
+ error listing the ids that do — "it did nothing" and "you typed it wrong" would
2319
+ otherwise look identical.
2320
+
2321
+ There is no `--required` flag, deliberately. "Required" would have to mean *this
2322
+ app does not run without it*, and that is a property of your app rather than of
2323
+ the codemod: the pages rename is unavoidable for a project with pages and
2324
+ irrelevant to an api-only one. You know which ones you need; we would be guessing.
2325
+
2232
2326
  ## In a workspace, the whole workspace moves
2233
2327
 
2234
2328
  Run `voltro update` anywhere inside a workspace — a `pnpm-workspace.yaml`, or a
@@ -2299,6 +2393,26 @@ voltro update --codemods-only --from 0.5.0 --to 0.6.0
2299
2393
 
2300
2394
  Every `@voltro/*` entry in `dependencies` and `devDependencies` — in every workspace member, see above — with the range style preserved (`^0.3.0` stays caret, `~0.3.0` stays tilde) unless you pass `--exact`. Non-registry specs (`workspace:*`, `catalog:`, `link:`, …) are left untouched — they're already resolved by your monorepo or catalog.
2301
2395
 
2396
+ ## When the install cannot run on this host
2397
+
2398
+ Some projects install in a container with their own store, from an offline
2399
+ mirror, or in a locked-down CI image. `voltro update` runs your package manager
2400
+ on the machine you invoke it from, so on those hosts the install step fails —
2401
+ and it fails *after* the version bump is written, which leaves the tree
2402
+ half-upgraded.
2403
+
2404
+ `--no-install` splits the command where those projects need it split:
2405
+
2406
+ ```bash
2407
+ voltro update --no-install --to 0.14.0 # writes the bump, stops, says what is left
2408
+ # ...install however this project installs...
2409
+ voltro update --codemods-only --from 0.13.0 --to 0.14.0
2410
+ ```
2411
+
2412
+ Step two is not optional and the command says so: the codemods for a jump ship
2413
+ **inside** the target version, so nothing can run them until the install has put
2414
+ that version on disk.
2415
+
2302
2416
  ## Which package manager
2303
2417
 
2304
2418
  `voltro update` never assumes npm. It resolves your project's package manager in this order, starting in the app directory and walking **up to the repo root**: