@voltro/cli 0.55.0 → 0.57.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.
- package/CHANGELOG.md +691 -0
- package/bin/voltro.mjs +24 -0
- package/dist/apiBuild-B83Cb2Rv.js +2 -0
- package/dist/{apiBuild-CMvLJM_K.js → apiBuild-DDr2aNFd.js} +119 -88
- package/dist/bin.js +1 -1
- package/dist/{build-S0QOzqPT.js → build-QKP6Bm0J.js} +308 -279
- package/dist/buildReport-52gHKgfO.js +64 -0
- package/dist/{checkCommand-DNkY5kwF.js → checkCommand-BISqx1OJ.js} +1 -1
- package/dist/{checkCommand-fbj9GDjN.js → checkCommand-DUtMWjcR.js} +6 -6
- package/dist/{clusterCmd-DrVFCzSj.js → clusterCmd-CdLB1GkT.js} +1 -1
- package/dist/{codegen-SIepQtUl.js → codegen-Bth5lUTU.js} +2 -1
- package/dist/codegen-DbH7NbCR.js +2 -0
- package/dist/{codegenCommand-3TDJezom.js → codegenCommand-DnVuDxwT.js} +11 -11
- package/dist/{codemodRunner-C2zxZUIw.js → codemodRunner-BlQPfjzA.js} +222 -0
- package/dist/{commands-BBYJ7Q3B.js → commands-CRbxgxv0.js} +35 -35
- package/dist/{dashboardCommand-D2kmyCLL.js → dashboardCommand-Bf_-Ne3P.js} +5 -5
- package/dist/{dataCommand-BEPPQiTl.js → dataCommand-BoBJJ-Gb.js} +3 -3
- package/dist/{dbCommand-DZTmOFT4.js → dbCommand-CMAIz-Bf.js} +457 -441
- package/dist/dbCommand-DHi_RuDl.js +2 -0
- package/dist/dev-Bl9HqtV7.js +3 -0
- package/dist/{dev-Ca_A_S9v.js → dev-DOEJXicj.js} +2623 -2485
- package/dist/{doctorCommand-CGZJK_4o.js → doctorCommand-BrWu67JZ.js} +524 -280
- package/dist/doctorCommand-DrQv9SL3.js +2 -0
- package/dist/{dormancyCommand-DY2rYpTa.js → dormancyCommand-xn2y-pJm.js} +1 -1
- package/dist/{embeddingsCommand-BoCqZsgp.js → embeddingsCommand-Cn5MbRDM.js} +1 -1
- package/dist/emptyResultHeadline-Csa5fZOF.js +18 -0
- package/dist/{envCommand-Bxy2fOjc.js → envCommand-UJmJIbs9.js} +8 -8
- package/dist/{evolveCommand-BsbZ-XDg.js → evolveCommand-Db30twUy.js} +2 -2
- package/dist/frameworkTableAssembly-B96WCNJA.js +2 -0
- package/dist/{frameworkTableAssembly-Do-cf6RJ.js → frameworkTableAssembly-vfkzuzEo.js} +131 -104
- package/dist/index.js +1 -1
- package/dist/{infoCommand-EmM3jPKD.js → infoCommand-BjVXpMlP.js} +1 -1
- package/dist/inspect-B0hL41s0.js +2 -0
- package/dist/inspect-DZnan87F.js +1500 -0
- package/dist/{inspectCmd-niF97fAq.js → inspectCmd-CP-G0sVK.js} +1 -1
- package/dist/{inspectFetch-EMuhTG_9.js → inspectFetch-BU1NyzxV.js} +36 -24
- package/dist/{inspectMetrics-CGF94puw.js → inspectMetrics-BY0Sjb2F.js} +19 -19
- package/dist/{logsCmd-B6oNsfaZ.js → logsCmd-C5y9HyrG.js} +59 -54
- package/dist/{manifestBuild-DjX5MoXy.js → manifestBuild-DnbFKF6w.js} +1 -1
- package/dist/manifestBuild-UXrnUcXP.js +2 -0
- package/dist/{migrate-CGFZS-1a.js → migrate-DtC3lu7H.js} +4 -4
- package/dist/precompressAssets-YhTi1aWp.js +40 -0
- package/dist/{probeCommand-Bs3iVBSL.js → probeCommand-C5fuN6Z2.js} +2 -2
- package/dist/{runtimeTrace-C1BTpHGQ.js → runtimeTrace-BQL_lfz6.js} +1 -1
- package/dist/{scheduleCmd-DQRu6BZC.js → scheduleCmd-DYBUfo_T.js} +1 -1
- package/dist/{sdkgen-CXMwLg9n.js → sdkgen-CMUPrDjH.js} +1 -1
- package/dist/{seedRunner-Dgsiwk_e.js → seedRunner-DISBKow-.js} +16 -16
- package/dist/serveCommand-B_w-Mpb_.js +2544 -0
- package/dist/serveCommand-Dtb48ffg.js +2 -0
- package/dist/serveEntry.js +1 -1
- package/dist/{start-EOV7s1NZ.js → start-YaUehtDV.js} +580 -558
- package/dist/{start-DH7cat4-.js → start-s25GAIgn.js} +1 -1
- package/dist/startEntry.js +1 -1
- package/dist/staticCachePolicy-CIyj6DbS.js +15 -0
- package/dist/{test-DO27-x2P.js → test-jipIQ5Mx.js} +1 -1
- package/dist/{tracesCmd-DgtgOUdi.js → tracesCmd-C1qKa94g.js} +69 -64
- package/dist/{updateCommand-C_jN1w18.js → updateCommand-C9n_Z_oG.js} +8 -2
- package/dist/updateCommand-DsXEAHbd.js +2 -0
- package/dist/webDev-B6ZMX42w.js +2 -0
- package/dist/{webDev-B7vNj4Bq.js → webDev-BgdkyjP6.js} +1379 -1303
- package/dist/{webhooksCommand-B1LVcyO3.js → webhooksCommand-CYXTNvXq.js} +2 -2
- package/dist/{workflowsCmd-BGF-mRZ5.js → workflowsCmd-g-DNpaUc.js} +1 -1
- package/package.json +55 -19
- package/templates/AGENTS.core.md +20 -1
- package/templates/AGENTS.md +21 -2
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/_manifest.json +2 -2
- package/templates/agent-docs/ai.md +1 -1
- package/templates/agent-docs/authentication.md +34 -19
- package/templates/agent-docs/cli.md +170 -0
- package/templates/agent-docs/configuration.md +29 -4
- package/templates/agent-docs/data.md +145 -5
- package/templates/agent-docs/database/scaling.md +50 -2
- package/templates/agent-docs/deployment.md +75 -1
- package/templates/agent-docs/internationalization.md +32 -0
- package/templates/agent-docs/local-first-mobile.md +9 -2
- package/templates/agent-docs/observability.md +227 -0
- package/templates/agent-docs/plugins/audit.md +21 -5
- package/templates/agent-docs/plugins/billing.md +17 -0
- package/templates/agent-docs/plugins/broadcast.md +2 -1
- package/templates/agent-docs/plugins/ratelimit.md +6 -1
- package/templates/agent-docs/plugins/row-history.md +11 -0
- package/templates/agent-docs/plugins.md +19 -0
- package/templates/agent-docs/reference.md +1 -0
- package/templates/agent-docs/scheduling.md +23 -0
- package/templates/agent-docs/schema-driven-ui.md +81 -0
- package/templates/agent-docs/templates/appshells.md +3 -3
- package/templates/agent-docs/whats-new.md +133 -77
- package/templates/apps/api-ai/package.json +6 -6
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +9 -9
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-row-history/package.json +8 -8
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +7 -7
- package/templates/apps/changelog/src/pages/[locale]/page.tsx +7 -1
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +7 -8
- package/templates/apps/frontend-admin/src/pages/(marketing)/layout.tsx +1 -2
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +1 -2
- package/templates/apps/frontend-app/package.json +8 -9
- package/templates/apps/frontend-app/src/pages/layout.tsx +1 -2
- package/templates/apps/frontend-auth/package.json +7 -8
- package/templates/apps/frontend-auth/src/components/AuthShell.tsx +1 -2
- package/templates/apps/frontend-blank/package.json +6 -7
- package/templates/apps/frontend-blank/src/pages/layout.tsx +1 -2
- package/templates/apps/frontend-cms/package.json +8 -9
- package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +1 -2
- package/templates/apps/frontend-collab/README.md +7 -2
- package/templates/apps/frontend-collab/package.json +9 -10
- package/templates/apps/frontend-collab/src/pages/layout.tsx +1 -2
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +8 -4
- package/templates/apps/frontend-collab/src/pages/page.tsx +10 -5
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-contact/src/components/ContactForm.island.tsx +1 -1
- package/templates/apps/frontend-dashboard/package.json +6 -7
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/layout.tsx +1 -2
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.tsx +1 -2
- package/templates/apps/frontend-docs/package.json +8 -8
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/package.json +7 -8
- package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +1 -2
- package/templates/apps/frontend-saas/package.json +7 -8
- package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +1 -2
- package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +1 -2
- package/templates/apps/frontend-spa/package.json +6 -7
- package/templates/apps/frontend-spa/src/pages/layout.tsx +1 -2
- package/templates/apps/frontend-ssr/package.json +6 -7
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +1 -2
- package/templates/apps/frontend-ssr-api/package.json +7 -8
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +1 -2
- package/templates/apps/frontend-static-blog/package.json +8 -8
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.tsx +1 -1
- package/templates/apps/frontend-static-blog/src/pages/[locale]/page.tsx +7 -1
- package/templates/apps/frontend-status/package.json +7 -8
- package/templates/apps/frontend-status/src/pages/layout.tsx +1 -2
- package/templates/apps/mobile-app/package.json +4 -4
- package/templates/baselines/compose/docker/api.Dockerfile +61 -5
- package/templates/baselines/compose/docker/web.Dockerfile +55 -10
- package/templates/baselines/compose-mariadb/docker/api.Dockerfile +61 -5
- package/templates/baselines/compose-mariadb/docker/web.Dockerfile +55 -10
- package/dist/apiBuild-Cl0IDx8c.js +0 -2
- package/dist/codegen-CN6vMM4J.js +0 -2
- package/dist/dbCommand-BTyBGhIA.js +0 -2
- package/dist/dev-DfVZaoys.js +0 -3
- package/dist/doctorCommand-djmqEcDC.js +0 -2
- package/dist/frameworkTableAssembly-Df2Ymp2f.js +0 -2
- package/dist/inspect-DCqILJ1G.js +0 -1197
- package/dist/inspect-DGJwpOAb.js +0 -2
- package/dist/manifestBuild-CJ2zvPvT.js +0 -2
- package/dist/serveCommand-C7IrCD58.js +0 -2445
- package/dist/serveCommand-Cjt5S9hD.js +0 -2
- package/dist/updateCommand-nnFjDbl4.js +0 -2
- package/dist/webDev-1XpVnYkW.js +0 -2
|
@@ -222,8 +222,8 @@ createVerifier({ secret: [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET
|
|
|
222
222
|
...t === void 0 ? {} : { payload: t }
|
|
223
223
|
};
|
|
224
224
|
}, S = u({ scope: "voltro:webhooks" }), C = ["--out", "--name"], w = async (e) => {
|
|
225
|
-
let { walk: t, loadDiscovered: n } = await import("./dev-
|
|
226
|
-
return r((await n(await t(e))).events.map((e) => ({
|
|
225
|
+
let { walk: t, loadDiscovered: n } = await import("./dev-Bl9HqtV7.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
|
|
226
|
+
return r((await n(await t(e), { root: e })).events.map((e) => ({
|
|
227
227
|
file: e.file,
|
|
228
228
|
descriptor: e.descriptor
|
|
229
229
|
}))).map((e) => x(e.descriptor));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as e } from "./cliOutput-D1tSBoRM.js";
|
|
2
|
-
import { n as t, o as n, r, t as i } from "./inspectFetch-
|
|
2
|
+
import { n as t, o as n, r, t as i } from "./inspectFetch-BU1NyzxV.js";
|
|
3
3
|
import { ansi as a } from "@voltro/logger";
|
|
4
4
|
//#region src/workflowsCmd.ts
|
|
5
5
|
var o = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.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",
|
|
@@ -766,6 +766,42 @@
|
|
|
766
766
|
"title": "WidgetKind gained 'rich-text' — total widget registries need one new entry",
|
|
767
767
|
"kind": "manual"
|
|
768
768
|
},
|
|
769
|
+
{
|
|
770
|
+
"version": "0.56.0",
|
|
771
|
+
"id": "0.56.0/01_inspect_answers_are_observations",
|
|
772
|
+
"title": "inspect answers are `Observation`s — the payload is `.data`",
|
|
773
|
+
"kind": "manual"
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
"version": "0.56.0",
|
|
777
|
+
"id": "0.56.0/02_broadcast_gap_is_one_object",
|
|
778
|
+
"title": "attachBroadcastBus's onGap now receives a single BroadcastGap object",
|
|
779
|
+
"kind": "manual"
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
"version": "0.56.0",
|
|
783
|
+
"id": "0.56.0/03_outbox_drain_needs_an_identity",
|
|
784
|
+
"title": "drainOutbox now requires claimedBy — the fleet-wide exactly-once gate",
|
|
785
|
+
"kind": "manual"
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
"version": "0.56.0",
|
|
789
|
+
"id": "0.56.0/04_reaction_dedupe_is_one_claim",
|
|
790
|
+
"title": "runReaction's dedupe is a single atomic claim",
|
|
791
|
+
"kind": "manual"
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
"version": "0.56.0",
|
|
795
|
+
"id": "0.56.0/05_plugin_bind_context_claims_a_change",
|
|
796
|
+
"title": "PluginBindContext requires claimChange; change identity moved to @voltro/database",
|
|
797
|
+
"kind": "manual"
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
"version": "0.56.0",
|
|
801
|
+
"id": "0.56.0/06_serve_is_api_only",
|
|
802
|
+
"title": "`voltro serve` on a WEB app now refuses — use `voltro start`",
|
|
803
|
+
"kind": "manual"
|
|
804
|
+
},
|
|
769
805
|
{
|
|
770
806
|
"version": "0.6.0",
|
|
771
807
|
"id": "0.6.0/01_no-dev-session-secret",
|
|
@@ -848,24 +884,24 @@
|
|
|
848
884
|
"@effect/platform-node": "^0.108.0",
|
|
849
885
|
"@effect/sql": "^0.52.0",
|
|
850
886
|
"@effect/workflow": "^0.19.0",
|
|
851
|
-
"@voltro/ai": "0.
|
|
852
|
-
"@voltro/cache": "0.
|
|
853
|
-
"@voltro/client": "0.
|
|
854
|
-
"@voltro/content": "0.
|
|
855
|
-
"@voltro/data-transfer": "0.
|
|
856
|
-
"@voltro/database": "0.
|
|
857
|
-
"@voltro/env": "0.
|
|
858
|
-
"@voltro/kv": "0.
|
|
859
|
-
"@voltro/logger": "0.
|
|
860
|
-
"@voltro/plugin-auth": "0.
|
|
861
|
-
"@voltro/plugin-broadcast": "0.
|
|
862
|
-
"@voltro/plugin-mail": "0.
|
|
863
|
-
"@voltro/plugin-storage": "0.
|
|
864
|
-
"@voltro/plugin-webhooks": "0.
|
|
865
|
-
"@voltro/protocol": "0.
|
|
866
|
-
"@voltro/runtime": "0.
|
|
867
|
-
"@voltro/serverless": "0.
|
|
868
|
-
"@voltro/workflow": "0.
|
|
887
|
+
"@voltro/ai": "0.57.0",
|
|
888
|
+
"@voltro/cache": "0.57.0",
|
|
889
|
+
"@voltro/client": "0.57.0",
|
|
890
|
+
"@voltro/content": "0.57.0",
|
|
891
|
+
"@voltro/data-transfer": "0.57.0",
|
|
892
|
+
"@voltro/database": "0.57.0",
|
|
893
|
+
"@voltro/env": "0.57.0",
|
|
894
|
+
"@voltro/kv": "0.57.0",
|
|
895
|
+
"@voltro/logger": "0.57.0",
|
|
896
|
+
"@voltro/plugin-auth": "0.57.0",
|
|
897
|
+
"@voltro/plugin-broadcast": "0.57.0",
|
|
898
|
+
"@voltro/plugin-mail": "0.57.0",
|
|
899
|
+
"@voltro/plugin-storage": "0.57.0",
|
|
900
|
+
"@voltro/plugin-webhooks": "0.57.0",
|
|
901
|
+
"@voltro/protocol": "0.57.0",
|
|
902
|
+
"@voltro/runtime": "0.57.0",
|
|
903
|
+
"@voltro/serverless": "0.57.0",
|
|
904
|
+
"@voltro/workflow": "0.57.0",
|
|
869
905
|
"chokidar": "^5.0.0",
|
|
870
906
|
"ioredis": "^5.11.1",
|
|
871
907
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.core.md
CHANGED
|
@@ -142,6 +142,24 @@ line below replaces something real apps write by hand hundreds of times.
|
|
|
142
142
|
notify-helper at the tail of the mutation body instead makes the reactivity
|
|
143
143
|
invisible — you can only find it by reading every executor. Both are
|
|
144
144
|
best-effort, so genuinely critical delivery still belongs in a workflow.
|
|
145
|
+
**A subscriber runs on EVERY replica.** That is right for a handler that
|
|
146
|
+
refreshes an index or drops a process-local cache, and wrong for one whose
|
|
147
|
+
effect IS a write someone receives — two pods send two mails. When the
|
|
148
|
+
handler is an effect, add `once:` (a deterministic key per change) and
|
|
149
|
+
exactly one replica runs it:
|
|
150
|
+
```ts
|
|
151
|
+
defineSubscriber({
|
|
152
|
+
table: 'absence_requests', on: ['insert'],
|
|
153
|
+
once: true,
|
|
154
|
+
handler: notifyApprovers,
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
`once: true` derives the key from the change (content + its position among
|
|
158
|
+
content-identical repeats) — prefer it. A function is for when you want to be
|
|
159
|
+
COARSER than one-per-change; a key you write must tell two genuine changes
|
|
160
|
+
apart, and a row id does not for `update`. `once` is AT MOST once — a replica
|
|
161
|
+
that wins and dies takes the event with it. `defineReaction`'s `dedupeKey`
|
|
162
|
+
gives its act the same fleet-wide gate.
|
|
145
163
|
4. **A roll-up / counter recomputed on every read?** → **`defineAggregate`**
|
|
146
164
|
(`*.aggregate.ts`). Add `incremental:` for `count|sum|avg|min|max` group-bys
|
|
147
165
|
and it is MAINTAINED on write rather than recomputed. `read({ where })`
|
|
@@ -217,6 +235,7 @@ line below replaces something real apps write by hand hundreds of times.
|
|
|
217
235
|
| `const rows = …; if (!rows[0]) throw new NotFound()` | `.one()` |
|
|
218
236
|
| 3+ sequential `store.query` to assemble related data | `relations()` + `.with()` |
|
|
219
237
|
| a notify/webhook helper called at the end of a mutation | `defineSubscriber` / `defineReaction` |
|
|
238
|
+
| a subscriber that sends a mail / notification / webhook | the same, plus **`once:`** — without it, one per replica |
|
|
220
239
|
| `WHERE ownerId = me` in every list handler | `setRowFilter` (covers the SUBSCRIPTION too) |
|
|
221
240
|
| a counter recomputed by scanning rows on every read | `defineAggregate` (+ `incremental:`) |
|
|
222
241
|
| `requireScope(...)` as the first line of every executor | `guards:` on the descriptor |
|
|
@@ -358,7 +377,7 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
|
|
|
358
377
|
| `*.workflow.tsx` + `*.workflow.server.tsx` | durable multi-step work |
|
|
359
378
|
| `*.trigger.tsx` | domain event → workflow |
|
|
360
379
|
| `*.cron.tsx` | scheduled job (single file) |
|
|
361
|
-
| `*.subscribe.ts` | best-effort post-commit reaction to a table |
|
|
380
|
+
| `*.subscribe.ts` | best-effort post-commit reaction to a table (every replica; `once:` for one) |
|
|
362
381
|
| `*.reaction.tsx` | standing reactive agent |
|
|
363
382
|
| `*.aggregate.ts` | scheduled materialised query |
|
|
364
383
|
| `*.startup.tsx` | run-once boot hook holding a resource |
|
package/templates/AGENTS.md
CHANGED
|
@@ -142,6 +142,24 @@ line below replaces something real apps write by hand hundreds of times.
|
|
|
142
142
|
notify-helper at the tail of the mutation body instead makes the reactivity
|
|
143
143
|
invisible — you can only find it by reading every executor. Both are
|
|
144
144
|
best-effort, so genuinely critical delivery still belongs in a workflow.
|
|
145
|
+
**A subscriber runs on EVERY replica.** That is right for a handler that
|
|
146
|
+
refreshes an index or drops a process-local cache, and wrong for one whose
|
|
147
|
+
effect IS a write someone receives — two pods send two mails. When the
|
|
148
|
+
handler is an effect, add `once:` (a deterministic key per change) and
|
|
149
|
+
exactly one replica runs it:
|
|
150
|
+
```ts
|
|
151
|
+
defineSubscriber({
|
|
152
|
+
table: 'absence_requests', on: ['insert'],
|
|
153
|
+
once: true,
|
|
154
|
+
handler: notifyApprovers,
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
`once: true` derives the key from the change (content + its position among
|
|
158
|
+
content-identical repeats) — prefer it. A function is for when you want to be
|
|
159
|
+
COARSER than one-per-change; a key you write must tell two genuine changes
|
|
160
|
+
apart, and a row id does not for `update`. `once` is AT MOST once — a replica
|
|
161
|
+
that wins and dies takes the event with it. `defineReaction`'s `dedupeKey`
|
|
162
|
+
gives its act the same fleet-wide gate.
|
|
145
163
|
4. **A roll-up / counter recomputed on every read?** → **`defineAggregate`**
|
|
146
164
|
(`*.aggregate.ts`). Add `incremental:` for `count|sum|avg|min|max` group-bys
|
|
147
165
|
and it is MAINTAINED on write rather than recomputed. `read({ where })`
|
|
@@ -217,6 +235,7 @@ line below replaces something real apps write by hand hundreds of times.
|
|
|
217
235
|
| `const rows = …; if (!rows[0]) throw new NotFound()` | `.one()` |
|
|
218
236
|
| 3+ sequential `store.query` to assemble related data | `relations()` + `.with()` |
|
|
219
237
|
| a notify/webhook helper called at the end of a mutation | `defineSubscriber` / `defineReaction` |
|
|
238
|
+
| a subscriber that sends a mail / notification / webhook | the same, plus **`once:`** — without it, one per replica |
|
|
220
239
|
| `WHERE ownerId = me` in every list handler | `setRowFilter` (covers the SUBSCRIPTION too) |
|
|
221
240
|
| a counter recomputed by scanning rows on every read | `defineAggregate` (+ `incremental:`) |
|
|
222
241
|
| `requireScope(...)` as the first line of every executor | `guards:` on the descriptor |
|
|
@@ -358,7 +377,7 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
|
|
|
358
377
|
| `*.workflow.tsx` + `*.workflow.server.tsx` | durable multi-step work |
|
|
359
378
|
| `*.trigger.tsx` | domain event → workflow |
|
|
360
379
|
| `*.cron.tsx` | scheduled job (single file) |
|
|
361
|
-
| `*.subscribe.ts` | best-effort post-commit reaction to a table |
|
|
380
|
+
| `*.subscribe.ts` | best-effort post-commit reaction to a table (every replica; `once:` for one) |
|
|
362
381
|
| `*.reaction.tsx` | standing reactive agent |
|
|
363
382
|
| `*.aggregate.ts` | scheduled materialised query |
|
|
364
383
|
| `*.startup.tsx` | run-once boot hook holding a resource |
|
|
@@ -730,7 +749,7 @@ each plugin's own README.
|
|
|
730
749
|
|
|
731
750
|
| Topic | Open | Summary |
|
|
732
751
|
|---|---|---|
|
|
733
|
-
| **What's new in 0.
|
|
752
|
+
| **What's new in 0.57.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. |
|
|
734
753
|
| 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. |
|
|
735
754
|
| 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. |
|
|
736
755
|
| 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.
|
|
12
|
+
| **What's new in 0.57.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. |
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
"group": null,
|
|
207
207
|
"description": "OpenTelemetry tracing in Voltro — the auto-emitted spans for every primitive, span attributes and nesting, the three enabling modes (console / OTLP / buffer), and adding your own spans with Effect.withSpan.",
|
|
208
208
|
"path": "agent-docs/observability.md",
|
|
209
|
-
"files":
|
|
209
|
+
"files": 5
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
212
|
"id": "plugins",
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
"group": null,
|
|
261
261
|
"description": "Project the typed descriptor graph into UI — forms, tables, pickers, and reactive components, all bound to a descriptor with near-zero glue.",
|
|
262
262
|
"path": "agent-docs/schema-driven-ui.md",
|
|
263
|
-
"files":
|
|
263
|
+
"files": 24
|
|
264
264
|
},
|
|
265
265
|
{
|
|
266
266
|
"id": "security",
|
|
@@ -1326,7 +1326,7 @@ defaults to treating an `AgentEvent`-shaped `{ _tag: 'done' | 'error' }` as the
|
|
|
1326
1326
|
end of the run. Override `isTerminal` when your element type signals completion
|
|
1327
1327
|
some other way, or the hook will keep trying to resume a finished stream.
|
|
1328
1328
|
|
|
1329
|
-
For multi-node deployments use `dataStoreResumableStreamStore(ctx.store)` — it persists to the framework's own database (`
|
|
1329
|
+
For multi-node deployments use `dataStoreResumableStreamStore(ctx.store)` — it persists to the framework's own database (`_voltro_stream_events` + `_voltro_stream_state`, created for you: the table assembly adds both for any app that depends on `@voltro/ai`, so `voltro dev` / `voltro db apply` / `voltro migrate` all plan them) and elects exactly ONE producer per `streamId` via an atomic claim, so only one node runs the model while every node's consumers tail the shared log. Sweep finished streams with `gcResumableStreams(store, { olderThan })`.
|
|
1330
1330
|
|
|
1331
1331
|
For the fastest path, `redisResumableStreamStore(redis, { ttlSeconds })` backs the log with a Redis LIST (`RPUSH`/`LRANGE`) plus a `SET … NX` producer claim — TTL evicts finished/abandoned streams without a sweep. `@voltro/ai` takes no Redis dependency; you inject a tiny `ResumableRedis` client (five methods: `setNx` / `rpush` / `lrange` / `set` / `exists`) adapting ioredis / node-redis. All three backends satisfy the same `ResumableStreamStore` interface, so they swap without touching the producer/consumer code.
|
|
1332
1332
|
|
|
@@ -2691,31 +2691,46 @@ comment, and this one is load-bearing: the resume grant is issued on its
|
|
|
2691
2691
|
strength. Omit `tables:` entirely and nothing is verified and nothing resumes —
|
|
2692
2692
|
the conservative default.
|
|
2693
2693
|
|
|
2694
|
-
## Eager loads are
|
|
2694
|
+
## Eager loads are narrowed too
|
|
2695
2695
|
|
|
2696
2696
|
A relation pulled in with `.with(...)` is resolved **below** the seam that
|
|
2697
|
-
|
|
2698
|
-
store recurses through its own raw read; the SQL stores
|
|
2699
|
-
one join). So a
|
|
2700
|
-
unfiltered
|
|
2697
|
+
AND-merges the filter onto a read's base table: the stores expand the eager tree
|
|
2698
|
+
themselves (the memory store recurses through its own raw read; the SQL stores
|
|
2699
|
+
fold the relation into one join or JSON aggregate). So for a while a filtered
|
|
2700
|
+
table reached through a relation came back unfiltered, and then — once that was
|
|
2701
|
+
found — the read refused rather than serve those rows.
|
|
2701
2702
|
|
|
2702
|
-
|
|
2703
|
-
|
|
2703
|
+
Neither is the case now. The middleware writes your filter's predicate into each
|
|
2704
|
+
narrowed branch's `where`, which is where a caller could have put it themselves,
|
|
2705
|
+
so every resolver on every dialect applies it without knowing a row filter
|
|
2706
|
+
exists:
|
|
2704
2707
|
|
|
2708
|
+
```ts
|
|
2709
|
+
// what you wrote
|
|
2710
|
+
notes.with({ readers: true })
|
|
2711
|
+
|
|
2712
|
+
// what the store resolves, when your filter narrows `readers`
|
|
2713
|
+
notes.with({ readers: { where: /* your filter's predicate for `readers` */ } })
|
|
2705
2714
|
```
|
|
2706
|
-
row filter: 'readers' is reached through an eager load on 'notes', and an eager
|
|
2707
|
-
relation is resolved BELOW the row filter — its rows would come back
|
|
2708
|
-
unfiltered. Refusing the read rather than serving it.
|
|
2709
|
-
→ read 'readers' as its own query (it is filtered there), or
|
|
2710
|
-
→ drop 'readers' from this .with(...) if the relation does not need
|
|
2711
|
-
row-level narrowing.
|
|
2712
|
-
```
|
|
2713
2715
|
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2716
|
+
Your own `where` is kept and the filter goes **under** it, so a branch you
|
|
2717
|
+
narrowed stays narrower and nothing a caller writes can widen the filter.
|
|
2718
|
+
Nested `.with(...)` is narrowed at every level. A relation reaching a table your
|
|
2719
|
+
filter does not narrow is untouched.
|
|
2720
|
+
|
|
2721
|
+
**One case still refuses: a filtered `manyToMany` JUNCTION.** A branch `where`
|
|
2722
|
+
is a predicate on the relation's TARGET, and a filter narrowing the junction has
|
|
2723
|
+
nowhere to be expressed — so that read raises rather than joining junction rows
|
|
2724
|
+
the filter would have removed:
|
|
2725
|
+
|
|
2726
|
+
```
|
|
2727
|
+
row filter: the junction table 'noteTags' behind relation 'tags' on 'notes' is
|
|
2728
|
+
row-filtered, and a many-to-many junction is read below the seam that could
|
|
2729
|
+
narrow it — its rows would be joined unfiltered. Refusing the read rather than
|
|
2730
|
+
serving it.
|
|
2731
|
+
→ read 'noteTags' as its own query (it is filtered there), or
|
|
2732
|
+
→ drop 'tags' from this .with(...).
|
|
2733
|
+
```
|
|
2719
2734
|
|
|
2720
2735
|
## Row filters vs. guards
|
|
2721
2736
|
|
|
@@ -1441,6 +1441,86 @@ Type mapping is faithful: `string → String`, `integer → Int`, `number → Do
|
|
|
1441
1441
|
|
|
1442
1442
|
**Scope — this is the SDK code generator, not a native runtime.** Deliberately out of scope (they need a native runtime or managed infra, not generated client code): native module bindings (camera, biometrics), the APNs/FCM push **sender** (per-tenant Apple/Firebase credentials, provisioned server-side), and the managed OTA / EAS build pipeline. The generated source is verified at the generator level (golden-string tests over the emitted Swift + Kotlin). Compiling it with `swiftc` / Gradle is the remaining step in your own mobile CI — the framework harness has no Swift/Kotlin toolchain.
|
|
1443
1443
|
|
|
1444
|
+
## Serving: asset prefix, pre-compression, sourcemaps, keep-alive
|
|
1445
|
+
|
|
1446
|
+
Four knobs that decide what leaves the container. All are `voltro start`
|
|
1447
|
+
concerns; none change what your code does.
|
|
1448
|
+
|
|
1449
|
+
```ts
|
|
1450
|
+
// apps/web/app.config.ts
|
|
1451
|
+
web: {
|
|
1452
|
+
// Serve `assets/` from a CDN. Becomes vite's `base`, so every emitted URL —
|
|
1453
|
+
// entry, modulepreloads, CSS, images, fonts — is written with the prefix at
|
|
1454
|
+
// BUILD time. Pre-rendered HTML still comes from the app; upload
|
|
1455
|
+
// `dist/assets/` to the prefix on deploy. Filenames are content-hashed, so a
|
|
1456
|
+
// previous deploy's assets stay valid for a visitor mid-navigation.
|
|
1457
|
+
assetPrefix: 'https://cdn.example.com/_assets',
|
|
1458
|
+
|
|
1459
|
+
// Emit `.map` files WITHOUT a `//# sourceMappingURL` comment, so nothing in
|
|
1460
|
+
// the shipped JS points at them. For `plugin-sentry`: upload them in your
|
|
1461
|
+
// deploy step and DELETE them before the image is built. `voltro start`
|
|
1462
|
+
// refuses to serve a `.map` regardless, so a forgotten delete is not a leak.
|
|
1463
|
+
sourcemaps: 'hidden',
|
|
1464
|
+
},
|
|
1465
|
+
http: {
|
|
1466
|
+
// Node hangs up an idle keep-alive connection after 5s; every proxy in front
|
|
1467
|
+
// holds one longer, and the request that lands in that window comes back as
|
|
1468
|
+
// a 502. Default 72000 clears nginx-60/ALB-60; raise it above YOUR proxy's
|
|
1469
|
+
// idle timeout. `headersTimeoutMs` must exceed it and is derived if omitted.
|
|
1470
|
+
keepAliveTimeoutMs: 72_000,
|
|
1471
|
+
},
|
|
1472
|
+
```
|
|
1473
|
+
|
|
1474
|
+
**Pre-compression is automatic.** `voltro build` writes `.br` (quality 11) and
|
|
1475
|
+
`.gz` beside every content-hashed asset over 1 KB, and `voltro start` serves the
|
|
1476
|
+
variant when the client accepts it. Measured on one 321 KB chunk, three requests:
|
|
1477
|
+
|
|
1478
|
+
| | time | bytes |
|
|
1479
|
+
|---|---:|---:|
|
|
1480
|
+
| compressed per request (before) | 5.6 / 5.0 / 4.6 ms | 100 665 |
|
|
1481
|
+
| pre-compressed (now) | 1.6 / 1.7 ms | **86 083** |
|
|
1482
|
+
|
|
1483
|
+
Faster *and* smaller: a build can afford brotli q11 where a per-request path
|
|
1484
|
+
cannot. Both encodings of one asset carry the same `ETag` — it is computed over
|
|
1485
|
+
the uncompressed file, so a shared cache sees one representation.
|
|
1486
|
+
|
|
1487
|
+
**A cross-origin api gets a `preconnect`.** When an api's `wsUrl` is on another
|
|
1488
|
+
host, the shell carries `<link rel="preconnect" href="…" crossorigin>` so DNS +
|
|
1489
|
+
TCP + TLS overlap the bundle download instead of following it. Same-origin apis
|
|
1490
|
+
are skipped — the browser already has that connection.
|
|
1491
|
+
|
|
1492
|
+
## `voltro serve <appDir>` — API apps only
|
|
1493
|
+
|
|
1494
|
+
`voltro serve` is the production server for an **API** app. On a **web** app it
|
|
1495
|
+
refuses and points you at `voltro start`:
|
|
1496
|
+
|
|
1497
|
+
```text
|
|
1498
|
+
`voltro serve` is the production server for an API app — this is a WEB app.
|
|
1499
|
+
|
|
1500
|
+
production voltro start .
|
|
1501
|
+
development voltro dev .
|
|
1502
|
+
```
|
|
1503
|
+
|
|
1504
|
+
That is a refusal, not a missing feature. `serve` used to build a web app a
|
|
1505
|
+
second time and hand it to `vite preview`, and both halves were wrong:
|
|
1506
|
+
|
|
1507
|
+
- **In production it never ran.** The launcher's serve fast path requires
|
|
1508
|
+
`.framework/dist-api/serveBundle/serveEntry.js` — an artefact a web build does
|
|
1509
|
+
not produce — so a web app exited 1 pointing at a path that cannot exist,
|
|
1510
|
+
directly after a `voltro build` that had just succeeded.
|
|
1511
|
+
- **Below production it destroyed the build.** That second build had no
|
|
1512
|
+
`@tailwindcss/vite`, no image pipeline and no per-page islands entries. With
|
|
1513
|
+
Tailwind it aborted; without it, it succeeded — and since both builds write
|
|
1514
|
+
`.framework/dist`, which Vite empties, it deleted `dist/server`, the island
|
|
1515
|
+
shells and every pre-rendered page. `voltro start` could then not boot at all.
|
|
1516
|
+
|
|
1517
|
+
So the table is:
|
|
1518
|
+
|
|
1519
|
+
| app | development | production |
|
|
1520
|
+
|---|---|---|
|
|
1521
|
+
| web | `voltro dev` | `voltro start` |
|
|
1522
|
+
| api | `voltro dev` | `voltro serve` |
|
|
1523
|
+
|
|
1444
1524
|
## `voltro doctor` — preflight a production serve
|
|
1445
1525
|
|
|
1446
1526
|
Production `voltro serve` for an **API** app boots ONLY from the precompiled serve
|
|
@@ -1775,6 +1855,9 @@ It covers both halves of the stack:
|
|
|
1775
1855
|
| server | `requireScope(...)` at the top of an executor | `guards:` on the descriptor |
|
|
1776
1856
|
| server | a `token` / `secret` / `password` column with no encryption | `.encrypted()` |
|
|
1777
1857
|
| server | a notify / webhook helper called at a mutation's tail | `defineSubscriber` / `defineReaction` |
|
|
1858
|
+
| server | a `*.subscribe.ts` handler that writes or publishes, with no `once:` | `once: true` on `defineSubscriber` |
|
|
1859
|
+
| server | an executor builds a field its descriptor's `output` does not declare | add it to `output` — the struct IS the serializer |
|
|
1860
|
+
| server | a mutation writes a NULLABLE column through an input field that cannot be `null` | `Schema.optional(Schema.NullOr(...))` |
|
|
1778
1861
|
| server | `hasMore` + `limit + 1` | `paginateById` |
|
|
1779
1862
|
| server | `.getTime()` / `.toISOString()` mapping a row on the way out | `timestampMs` / `timestampMsOrNull` from `@voltro/database/wire` in the descriptor's `output` struct |
|
|
1780
1863
|
| client | per-field `useState` + a submit flag | `useFormBinding` |
|
|
@@ -1801,6 +1884,47 @@ already uses `.one()`, stays silent.
|
|
|
1801
1884
|
|
|
1802
1885
|
Two of them are worth spelling out, because their advice is not one-line:
|
|
1803
1886
|
|
|
1887
|
+
**The subscriber rule is the second half of the mutation-tail rule.** That one
|
|
1888
|
+
moves an effect OUT of a mutation and into a subscriber, which is right — and
|
|
1889
|
+
lands it on a channel every replica listens to. `store.onChange` is a broadcast:
|
|
1890
|
+
correct for a READER (a cache drop, an index refresh, a live query must run
|
|
1891
|
+
everywhere) and a multiplier for an EFFECT, because there is nothing to make
|
|
1892
|
+
idempotent — the effect IS the write, so each run produces another one. One
|
|
1893
|
+
`INSERT` behind two replicas therefore writes two notification rows, and four
|
|
1894
|
+
with a broadcast bus in front.
|
|
1895
|
+
|
|
1896
|
+
So the rule fires when a handler WRITES (`ctx.store.insert` / `update` /
|
|
1897
|
+
`upsert` / …), publishes (`ctx.publish`), or calls a `notify` / `sendWebhook` /
|
|
1898
|
+
`sendMail`-shaped helper, and the subscriber declares no `once:`. Any `once:`
|
|
1899
|
+
silences it — `true` or a key function — because the question is whether the
|
|
1900
|
+
decision was made, not which way. It reads the handler through the AST, so
|
|
1901
|
+
`handler: notifyApprovers` naming a function in the same file is judged exactly
|
|
1902
|
+
like an inline arrow; a handler IMPORTED from another module is not judged at
|
|
1903
|
+
all, since its body is not in the file being read.
|
|
1904
|
+
|
|
1905
|
+
**Two rules read a DECLARATION against a DECLARATION, and are therefore exact.**
|
|
1906
|
+
`executor-builds-an-undeclared-field` compares the object literal an executor
|
|
1907
|
+
returns with its descriptor's `output` struct: since the struct IS the
|
|
1908
|
+
serializer, a key it does not declare is stripped on the way out, so every
|
|
1909
|
+
reader downstream gets `undefined` and renders a blank with nothing logged
|
|
1910
|
+
anywhere. `nullable-column-a-mutation-cannot-clear` compares a mutation's target
|
|
1911
|
+
table's nullable columns with its input schema — a nullable column written
|
|
1912
|
+
through a field that cannot be `null` can be set once and never emptied.
|
|
1913
|
+
|
|
1914
|
+
Both REFUSE rather than guess, and the refusals are the interesting part. A
|
|
1915
|
+
spread on either side of the output comparison (`{ ...row, extra }`, or a
|
|
1916
|
+
`...Base` inside the declared struct) means the key set is not knowable from the
|
|
1917
|
+
source; reporting the visible half would name the field the author can already
|
|
1918
|
+
see and miss the ones they cannot. An output that is a named schema rather than
|
|
1919
|
+
a literal struct is unjudgeable, not empty — the second reading would make every
|
|
1920
|
+
field a finding. And a table declaration the scan cannot find produces no
|
|
1921
|
+
finding at all, because "this table has no nullable columns" and "I could not
|
|
1922
|
+
look" lead to opposite conclusions.
|
|
1923
|
+
|
|
1924
|
+
It stays quiet on a reader on purpose. `once:` on a cache-warming subscriber
|
|
1925
|
+
would silence it on every replica but one, which is worse than the repetition it
|
|
1926
|
+
removes — only the handler's author knows which of the two they wrote.
|
|
1927
|
+
|
|
1804
1928
|
**The credential-column rule skips names that aren't credentials.** A name ending
|
|
1805
1929
|
in `Id` / `_id`, a name ending in `Hash` / `_hash`, and a name beginning with
|
|
1806
1930
|
`vault` are all left alone:
|
|
@@ -2265,6 +2389,33 @@ voltro check --url https://api.example.com
|
|
|
2265
2389
|
|
|
2266
2390
|
`--token` (or `VOLTRO_INSPECT_TOKEN`) supplies the bearer; `VOLTRO_INSPECT_URL` sets a default target so you can drop the flag. Works for `inspect`, `logs`, `traces`, `workflows`, `cluster` and `check`.
|
|
2267
2391
|
|
|
2392
|
+
### `logs` and `traces` need a ring on the target — off by default
|
|
2393
|
+
|
|
2394
|
+
A deployed app runs `voltro serve`, and `voltro serve` keeps **no** in-process log ring or span buffer unless you ask for one. Without it both commands answer:
|
|
2395
|
+
|
|
2396
|
+
```
|
|
2397
|
+
voltro logs: NOTHING WAS SEARCHED — the target could not be read: HTTP 404
|
|
2398
|
+
endpoint not enabled on this instance — This instance keeps no in-process logs ring…
|
|
2399
|
+
```
|
|
2400
|
+
|
|
2401
|
+
That headline is the point: *"no records matched the given filters"* would have sent you off widening `--tail` forever. Turn a ring on per app:
|
|
2402
|
+
|
|
2403
|
+
```ts
|
|
2404
|
+
// app.config.ts
|
|
2405
|
+
export default defineAppConfig({
|
|
2406
|
+
inspect: {
|
|
2407
|
+
logs: true, // default size
|
|
2408
|
+
traces: { size: 2_000 }, // or name it
|
|
2409
|
+
},
|
|
2410
|
+
})
|
|
2411
|
+
```
|
|
2412
|
+
|
|
2413
|
+
or per deployment, without a rebuild: `VOLTRO_INSPECT_LOGS=2000`, `VOLTRO_INSPECT_TRACES=on`, `=off` to override a config that declared one.
|
|
2414
|
+
|
|
2415
|
+
**Why off by default.** A ring is memory on every replica, forever, for data most deployments already collect from stdout through their platform's pipeline. That pipeline stays the main road; the ring answers the different question — *what has THIS instance been doing in the last few minutes* — and it answers it for a hosted app, where reading a pod's stdout is not an option.
|
|
2416
|
+
|
|
2417
|
+
**What a ring does not do.** It holds a bounded number of records on ONE replica. Against a fleet you are reading 1/N, and it is not retention: a restart empties it. `voltro dev` keeps both rings unconditionally — this switch is about what production does.
|
|
2418
|
+
|
|
2268
2419
|
## `voltro probe access` — is a declared guard actually enforced?
|
|
2269
2420
|
|
|
2270
2421
|
`voltro check` reads an app's manifest and reports a procedure with **no access
|
|
@@ -4548,6 +4699,25 @@ prints only because your tree matched it.
|
|
|
4548
4699
|
|
|
4549
4700
|
Codemods that span multiple versions run in order (e.g. upgrading `0.2.0 → 0.4.0` runs the `0.3.0` and `0.4.0` codemods in sequence).
|
|
4550
4701
|
|
|
4702
|
+
### When nothing changed, read WHICH nothing
|
|
4703
|
+
|
|
4704
|
+
Two states end an update with no diff, and they mean opposite things: the jump
|
|
4705
|
+
ships no codemods, or it ships some and every one of them decided your project
|
|
4706
|
+
is not affected. The summary names which:
|
|
4707
|
+
|
|
4708
|
+
```text
|
|
4709
|
+
codemods: 2 ship for this jump; none matched your project.
|
|
4710
|
+
· 0.55.0/01_target-relations-declare-columns — its own check found nothing to change
|
|
4711
|
+
· 0.55.0/02_widget-kind-gained-rich-text — its own check found nothing to change
|
|
4712
|
+
```
|
|
4713
|
+
|
|
4714
|
+
`none ship for this jump` is the first. The second lists the ids, because a
|
|
4715
|
+
codemod's own check is a predicate that can be wrong — and a gate that reads
|
|
4716
|
+
the wrong files answers "does not apply" for a project that is fully affected.
|
|
4717
|
+
If you recognise a subject in that list as something your app *does* use, that
|
|
4718
|
+
is a bug in the check rather than a fact about your code; the CHANGELOG entry
|
|
4719
|
+
for the version says what each one looks for.
|
|
4720
|
+
|
|
4551
4721
|
## The database is separate
|
|
4552
4722
|
|
|
4553
4723
|
`voltro update` does **not** touch your database. Framework-owned `_voltro_*` tables (workflow runs, schedules, …) are reconciled by the declarative differ, not by codemods: when a release changes one of those tables, your next `voltro db apply` (or `voltro dev` boot, which auto-applies) picks up the change.
|
|
@@ -202,10 +202,35 @@ is accepted but only reachable from server-side render code, and the boot
|
|
|
202
202
|
## Plugins declare their env
|
|
203
203
|
|
|
204
204
|
A plugin that reads environment variables declares them via `declaredEnv` on its
|
|
205
|
-
`VoltroPlugin
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
`VoltroPlugin`. The plugin still reads its own values; declaring them makes its
|
|
206
|
+
env needs visible in the manifest, `.env.example`, and the dashboard. The
|
|
207
|
+
first-party plugins already declare theirs, so the manifest is complete out of
|
|
208
|
+
the box.
|
|
209
|
+
|
|
210
|
+
**`required: true` is a boot CONTRACT, not documentation.** Every required entry
|
|
211
|
+
is resolved before any plugin activates, and one that does not resolve **aborts
|
|
212
|
+
the boot** with the plugin named:
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
plugin environment validation failed (1 variable):
|
|
216
|
+
• STRIPE_SECRET_KEY — required by @voltro/plugin-billing, and nothing set it
|
|
217
|
+
Stripe API secret key.
|
|
218
|
+
→ set the variable, or pass the value as a plugin option if the plugin takes one, or
|
|
219
|
+
declare it yourself in app.config.ts's `env` (an app declaration overrides a plugin's).
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
That is the point of failing at the boot: a deployment variable which silently
|
|
223
|
+
stops being set — a rotated secret, a typo in a values file, a CI variable nobody
|
|
224
|
+
created — is routine, and the alternative is finding out from the first customer.
|
|
225
|
+
|
|
226
|
+
A `secret: true` variable resolves through the configured **secrets backend**,
|
|
227
|
+
not just `process.env`. A backend that cannot answer fails the boot too, reported
|
|
228
|
+
as *unreadable* rather than as an absent variable — those are different facts and
|
|
229
|
+
need different next steps.
|
|
230
|
+
|
|
231
|
+
Your own `defineEnv` declaration of the same variable **overrides the plugin's**:
|
|
232
|
+
declare it there to give it a default, or to mark it optional because you pass
|
|
233
|
+
the value as a plugin option instead.
|
|
209
234
|
|
|
210
235
|
## Anti-pattern
|
|
211
236
|
|