@voltro/cli 0.54.0 → 0.56.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 +639 -2
- package/bin/voltro.mjs +24 -0
- package/dist/{apiBuild-DTWp0S_q.js → apiBuild-Bdaetr37.js} +119 -88
- package/dist/apiBuild-Vw1figjO.js +2 -0
- package/dist/bin.js +1 -1
- package/dist/{build-D4ygSbnV.js → build-CI36wL4R.js} +339 -314
- package/dist/buildReport-52gHKgfO.js +64 -0
- package/dist/{checkCommand-Dg1G7Gwd.js → checkCommand-CAwFXrxA.js} +6 -6
- package/dist/{checkCommand-L7DTlpIF.js → checkCommand-D0QV_zM_.js} +1 -1
- package/dist/{clusterCmd-DrVFCzSj.js → clusterCmd-CdLB1GkT.js} +1 -1
- package/dist/{codegen-DSLM8Su9.js → codegen-Bth5lUTU.js} +76 -64
- package/dist/codegen-DbH7NbCR.js +2 -0
- package/dist/{codegenCommand-CG_Vx4lc.js → codegenCommand-CidbQzbv.js} +15 -14
- package/dist/{codemodRunner-Cd4xkC6u.js → codemodRunner-BlQPfjzA.js} +277 -0
- package/dist/{commands-6Kzi92Np.js → commands-CWjfThXv.js} +35 -35
- package/dist/{dashboardCommand-Cq1PWvI1.js → dashboardCommand-BekcY5Ls.js} +3 -3
- package/dist/{dataCommand-DYzW8vkv.js → dataCommand-2pccgbIy.js} +267 -195
- package/dist/{dbCommand-B4NWZtGL.js → dbCommand-DpK_vQET.js} +457 -441
- package/dist/dbCommand-DrycGWWt.js +2 -0
- package/dist/{dev-cKUiZZsB.js → dev-B9Gz0k85.js} +1 -1
- package/dist/{dev-CmuvUKRq.js → dev-Dw263KPu.js} +2598 -2452
- package/dist/doctorCommand-BMWs6aVm.js +2 -0
- package/dist/{doctorCommand-DCiFVMtZ.js → doctorCommand-aR_bFmIi.js} +353 -251
- package/dist/{dormancyCommand-w1TrmgYP.js → dormancyCommand-eXTQMbHU.js} +1 -1
- package/dist/{embeddingsCommand-CMgPyRTr.js → embeddingsCommand-CTmiQvwa.js} +1 -1
- package/dist/{envCommand-Cyynmcfa.js → envCommand-BDUgV7EM.js} +12 -12
- package/dist/{evolveCommand-BwvQ8dVH.js → evolveCommand-YV8qW1LU.js} +2 -2
- package/dist/frameworkTableAssembly-CGNC0qr7.js +2 -0
- package/dist/{frameworkTableAssembly-D7LJuALW.js → frameworkTableAssembly-DNOFXfEQ.js} +102 -100
- package/dist/index.js +1 -1
- package/dist/{infoCommand-DlYlUPqs.js → infoCommand-BjVXpMlP.js} +1 -1
- package/dist/inspect-CNYvNXPU.js +1484 -0
- package/dist/inspect-S2rWy1Ys.js +2 -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/interruptedReplace-CwnkBb2X.js +41 -0
- package/dist/interruptedReplace-qzmFI020.js +2 -0
- package/dist/{logsCmd-B6oNsfaZ.js → logsCmd-BU8uCdys.js} +1 -1
- package/dist/{manifestBuild-Cqgsx2bM.js → manifestBuild-CEkjfpwc.js} +1 -1
- package/dist/manifestBuild-DIa_s6u0.js +2 -0
- package/dist/{migrate-BK_Bbx-_.js → migrate-SICulyz1.js} +2 -2
- package/dist/precompressAssets-YhTi1aWp.js +40 -0
- package/dist/{probeCommand-_C0YU207.js → probeCommand-6HxEkNDG.js} +2 -2
- package/dist/{runtimeTrace-CGWx1Q6l.js → runtimeTrace-DgYMc09E.js} +1 -1
- package/dist/{scheduleCmd-DQRu6BZC.js → scheduleCmd-DYBUfo_T.js} +1 -1
- package/dist/{sdkgen-CDGHQUFj.js → sdkgen-PY-umd6O.js} +1 -1
- package/dist/{seedRunner-Dgsiwk_e.js → seedRunner-DISBKow-.js} +16 -16
- package/dist/serveCommand-BITS8Hpj.js +2 -0
- package/dist/{serveCommand-Bje09q1v.js → serveCommand-DIJ3ma76.js} +951 -909
- package/dist/serveEntry.js +1 -1
- package/dist/{start-Clz-1BHB.js → start-B9NGB8gn.js} +627 -588
- package/dist/{start-B0bnJgxI.js → start-BFQQkL1i.js} +1 -1
- package/dist/startEntry.js +1 -1
- package/dist/staticCachePolicy-CIyj6DbS.js +15 -0
- package/dist/{test-D_kW4KMj.js → test-jipIQ5Mx.js} +1 -1
- package/dist/{tracesCmd-DgtgOUdi.js → tracesCmd-BWYDqMy6.js} +1 -1
- package/dist/{updateCommand-CRJlAOaM.js → updateCommand-C9n_Z_oG.js} +8 -2
- package/dist/updateCommand-DsXEAHbd.js +2 -0
- package/dist/webDev-C2dRz9s5.js +2 -0
- package/dist/{webDev-DSI9SOhs.js → webDev-C53hJdcL.js} +1265 -1288
- package/dist/{webhooksCommand-BvzXNHji.js → webhooksCommand-uuPu8qQX.js} +1 -1
- package/dist/{workflowsCmd-BGF-mRZ5.js → workflowsCmd-g-DNpaUc.js} +1 -1
- package/package.json +67 -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 +1 -1
- package/templates/agent-docs/authentication.md +49 -6
- package/templates/agent-docs/cli.md +216 -11
- package/templates/agent-docs/data.md +209 -18
- package/templates/agent-docs/database/scaling.md +40 -1
- package/templates/agent-docs/deployment.md +53 -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/billing.md +15 -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 +65 -8
- package/templates/agent-docs/reference.md +5 -3
- package/templates/agent-docs/routing.md +18 -0
- package/templates/agent-docs/schema-driven-ui.md +125 -0
- package/templates/agent-docs/templates/appshells.md +3 -3
- package/templates/agent-docs/whats-new.md +408 -106
- 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-CeUN55uk.js +0 -2
- package/dist/codegen-DjgxEOnD.js +0 -2
- package/dist/dbCommand-CSFWs9ev.js +0 -2
- package/dist/doctorCommand-J3qu4E0Y.js +0 -2
- package/dist/frameworkTableAssembly-IPD1pUnZ.js +0 -2
- package/dist/inspect-Bd8-9wsi.js +0 -1193
- package/dist/inspect-CuoDInfZ.js +0 -2
- package/dist/interruptedReplace-C3O3M1MM.js +0 -28
- package/dist/interruptedReplace-CvmiAM9K.js +0 -2
- package/dist/manifestBuild-C4-J1-m_.js +0 -2
- package/dist/serveCommand-BiPe8BJm.js +0 -2
- package/dist/updateCommand-CIoVDKnj.js +0 -2
- package/dist/webDev-DlvZO30c.js +0 -2
|
@@ -222,7 +222,7 @@ 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-
|
|
225
|
+
let { walk: t, loadDiscovered: n } = await import("./dev-B9Gz0k85.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
|
|
226
226
|
return r((await n(await t(e))).events.map((e) => ({
|
|
227
227
|
file: e.file,
|
|
228
228
|
descriptor: 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.56.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",
|
|
@@ -754,6 +754,54 @@
|
|
|
754
754
|
"title": "WidgetKind gained 'array' and 'reference' — total widget registries need two new entries",
|
|
755
755
|
"kind": "manual"
|
|
756
756
|
},
|
|
757
|
+
{
|
|
758
|
+
"version": "0.55.0",
|
|
759
|
+
"id": "0.55.0/01_target-relations-declare-columns",
|
|
760
|
+
"title": "A target's `relations:` names the junction's two columns now, not just the table",
|
|
761
|
+
"kind": "manual"
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
"version": "0.55.0",
|
|
765
|
+
"id": "0.55.0/02_widget-kind-gained-rich-text",
|
|
766
|
+
"title": "WidgetKind gained 'rich-text' — total widget registries need one new entry",
|
|
767
|
+
"kind": "manual"
|
|
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
|
+
},
|
|
757
805
|
{
|
|
758
806
|
"version": "0.6.0",
|
|
759
807
|
"id": "0.6.0/01_no-dev-session-secret",
|
|
@@ -836,24 +884,24 @@
|
|
|
836
884
|
"@effect/platform-node": "^0.108.0",
|
|
837
885
|
"@effect/sql": "^0.52.0",
|
|
838
886
|
"@effect/workflow": "^0.19.0",
|
|
839
|
-
"@voltro/ai": "0.
|
|
840
|
-
"@voltro/cache": "0.
|
|
841
|
-
"@voltro/client": "0.
|
|
842
|
-
"@voltro/content": "0.
|
|
843
|
-
"@voltro/data-transfer": "0.
|
|
844
|
-
"@voltro/database": "0.
|
|
845
|
-
"@voltro/env": "0.
|
|
846
|
-
"@voltro/kv": "0.
|
|
847
|
-
"@voltro/logger": "0.
|
|
848
|
-
"@voltro/plugin-auth": "0.
|
|
849
|
-
"@voltro/plugin-broadcast": "0.
|
|
850
|
-
"@voltro/plugin-mail": "0.
|
|
851
|
-
"@voltro/plugin-storage": "0.
|
|
852
|
-
"@voltro/plugin-webhooks": "0.
|
|
853
|
-
"@voltro/protocol": "0.
|
|
854
|
-
"@voltro/runtime": "0.
|
|
855
|
-
"@voltro/serverless": "0.
|
|
856
|
-
"@voltro/workflow": "0.
|
|
887
|
+
"@voltro/ai": "0.56.0",
|
|
888
|
+
"@voltro/cache": "0.56.0",
|
|
889
|
+
"@voltro/client": "0.56.0",
|
|
890
|
+
"@voltro/content": "0.56.0",
|
|
891
|
+
"@voltro/data-transfer": "0.56.0",
|
|
892
|
+
"@voltro/database": "0.56.0",
|
|
893
|
+
"@voltro/env": "0.56.0",
|
|
894
|
+
"@voltro/kv": "0.56.0",
|
|
895
|
+
"@voltro/logger": "0.56.0",
|
|
896
|
+
"@voltro/plugin-auth": "0.56.0",
|
|
897
|
+
"@voltro/plugin-broadcast": "0.56.0",
|
|
898
|
+
"@voltro/plugin-mail": "0.56.0",
|
|
899
|
+
"@voltro/plugin-storage": "0.56.0",
|
|
900
|
+
"@voltro/plugin-webhooks": "0.56.0",
|
|
901
|
+
"@voltro/protocol": "0.56.0",
|
|
902
|
+
"@voltro/runtime": "0.56.0",
|
|
903
|
+
"@voltro/serverless": "0.56.0",
|
|
904
|
+
"@voltro/workflow": "0.56.0",
|
|
857
905
|
"chokidar": "^5.0.0",
|
|
858
906
|
"ioredis": "^5.11.1",
|
|
859
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.56.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.56.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",
|
|
@@ -2617,10 +2617,14 @@ Optional, one line, and it buys back a feature the filter otherwise switches
|
|
|
2617
2617
|
off for the whole app:
|
|
2618
2618
|
|
|
2619
2619
|
```ts
|
|
2620
|
+
const OWNED = new Set(['documents', 'comments'])
|
|
2621
|
+
|
|
2620
2622
|
setRowFilter({
|
|
2621
2623
|
load,
|
|
2622
|
-
predicate,
|
|
2623
|
-
|
|
2624
|
+
predicate: (ctx, table) => (OWNED.has(table) ? eq('ownerId', ctx.userId) : undefined),
|
|
2625
|
+
// Derived from the same set the predicate reads. Two hand-kept lists is the
|
|
2626
|
+
// shape in which a table lands in exactly one of them.
|
|
2627
|
+
tables: [...OWNED],
|
|
2624
2628
|
})
|
|
2625
2629
|
```
|
|
2626
2630
|
|
|
@@ -2629,10 +2633,49 @@ is excluded for a subscription whose row set is re-resolved per delivery —
|
|
|
2629
2633
|
replaying deltas could serve rows the subject has since lost. Without a
|
|
2630
2634
|
declaration the framework cannot tell which tables your predicate may reach, so
|
|
2631
2635
|
it excludes them all: one registration disables cheap reconnects for every
|
|
2632
|
-
subscription in the process
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
+
subscription in the process, including every one reading a table your predicate
|
|
2637
|
+
never returns anything for. With the declaration, only subscriptions on the
|
|
2638
|
+
listed tables are excluded.
|
|
2639
|
+
|
|
2640
|
+
**What it does not buy, and this bounds the whole feature.** A subscription only
|
|
2641
|
+
has a delta chain when its executor returns a **descriptor**. One that returns a
|
|
2642
|
+
mapped value or a page envelope —
|
|
2643
|
+
|
|
2644
|
+
```ts
|
|
2645
|
+
export default async ({ database }) => {
|
|
2646
|
+
const rows = await database.notifications.where(...)
|
|
2647
|
+
return { notifications: rows.map(toDto), hasMore: rows.length === 20 }
|
|
2648
|
+
}
|
|
2649
|
+
```
|
|
2650
|
+
|
|
2651
|
+
— re-runs an opaque handler and emits snapshots, with or without a filter. So
|
|
2652
|
+
the count `tables:` gives back is the count of descriptor-returning
|
|
2653
|
+
subscriptions, not the number of queries in your app. Declare it anyway (it
|
|
2654
|
+
costs nothing, and it applies the moment such a query returns the builder), but
|
|
2655
|
+
measure before expecting a change.
|
|
2656
|
+
|
|
2657
|
+
**How to see which of yours are which.** The two shapes are indistinguishable
|
|
2658
|
+
from the outside — a subscription that resumed and one that was never eligible
|
|
2659
|
+
both reconnect with rows on the screen. So the runtime records its own verdict
|
|
2660
|
+
at the moment it decides, per query label:
|
|
2661
|
+
|
|
2662
|
+
```sh
|
|
2663
|
+
curl -s localhost:4000/_voltro/inspect/subscriptions | jq .resume
|
|
2664
|
+
```
|
|
2665
|
+
|
|
2666
|
+
```json
|
|
2667
|
+
[
|
|
2668
|
+
{ "label": "documents.list", "resumable": 12, "excluded": {} },
|
|
2669
|
+
{ "label": "notifications.list", "resumable": 0, "excluded": { "computed": 8 } },
|
|
2670
|
+
{ "label": "comments.list", "resumable": 0, "excluded": { "row-filter": 3 } }
|
|
2671
|
+
]
|
|
2672
|
+
```
|
|
2673
|
+
|
|
2674
|
+
A label appears once something has subscribed to it, so click through the app
|
|
2675
|
+
first. `computed` means no declaration can ever help that query; `row-filter`
|
|
2676
|
+
means the filter narrows its source and the exclusion is the point;
|
|
2677
|
+
`eager-load` means dropping the `.with(...)` would flip it. `voltro dev` also
|
|
2678
|
+
logs each verdict once per label under the `voltro:resume` scope.
|
|
2636
2679
|
|
|
2637
2680
|
**Why a declaration and not a probe.** Resolving the scope at subscribe time
|
|
2638
2681
|
and treating "returns `undefined` for this table" as safe is cheaper and
|
|
@@ -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,7 @@ 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` |
|
|
1778
1859
|
| server | `hasMore` + `limit + 1` | `paginateById` |
|
|
1779
1860
|
| server | `.getTime()` / `.toISOString()` mapping a row on the way out | `timestampMs` / `timestampMsOrNull` from `@voltro/database/wire` in the descriptor's `output` struct |
|
|
1780
1861
|
| client | per-field `useState` + a submit flag | `useFormBinding` |
|
|
@@ -1801,6 +1882,28 @@ already uses `.one()`, stays silent.
|
|
|
1801
1882
|
|
|
1802
1883
|
Two of them are worth spelling out, because their advice is not one-line:
|
|
1803
1884
|
|
|
1885
|
+
**The subscriber rule is the second half of the mutation-tail rule.** That one
|
|
1886
|
+
moves an effect OUT of a mutation and into a subscriber, which is right — and
|
|
1887
|
+
lands it on a channel every replica listens to. `store.onChange` is a broadcast:
|
|
1888
|
+
correct for a READER (a cache drop, an index refresh, a live query must run
|
|
1889
|
+
everywhere) and a multiplier for an EFFECT, because there is nothing to make
|
|
1890
|
+
idempotent — the effect IS the write, so each run produces another one. One
|
|
1891
|
+
`INSERT` behind two replicas therefore writes two notification rows, and four
|
|
1892
|
+
with a broadcast bus in front.
|
|
1893
|
+
|
|
1894
|
+
So the rule fires when a handler WRITES (`ctx.store.insert` / `update` /
|
|
1895
|
+
`upsert` / …), publishes (`ctx.publish`), or calls a `notify` / `sendWebhook` /
|
|
1896
|
+
`sendMail`-shaped helper, and the subscriber declares no `once:`. Any `once:`
|
|
1897
|
+
silences it — `true` or a key function — because the question is whether the
|
|
1898
|
+
decision was made, not which way. It reads the handler through the AST, so
|
|
1899
|
+
`handler: notifyApprovers` naming a function in the same file is judged exactly
|
|
1900
|
+
like an inline arrow; a handler IMPORTED from another module is not judged at
|
|
1901
|
+
all, since its body is not in the file being read.
|
|
1902
|
+
|
|
1903
|
+
It stays quiet on a reader on purpose. `once:` on a cache-warming subscriber
|
|
1904
|
+
would silence it on every replica but one, which is worse than the repetition it
|
|
1905
|
+
removes — only the handler's author knows which of the two they wrote.
|
|
1906
|
+
|
|
1804
1907
|
**The credential-column rule skips names that aren't credentials.** A name ending
|
|
1805
1908
|
in `Id` / `_id`, a name ending in `Hash` / `_hash`, and a name beginning with
|
|
1806
1909
|
`vault` are all left alone:
|
|
@@ -2969,17 +3072,67 @@ the swap could not run: 2 row(s) in the bundle reference a row the bundle does n
|
|
|
2969
3072
|
The target is UNCHANGED — the swap runs in one transaction and none of it committed.
|
|
2970
3073
|
```
|
|
2971
3074
|
|
|
2972
|
-
Staging tables
|
|
2973
|
-
|
|
2974
|
-
|
|
3075
|
+
### Staging tables a dead run left behind
|
|
3076
|
+
|
|
3077
|
+
A staged run RECORDS the scratch tables it creates, in the same
|
|
3078
|
+
`_voltro_replace_in_progress` table an interrupted destructive `replace` writes
|
|
3079
|
+
to — with one difference that matters: **a staging record never refuses a boot.**
|
|
3080
|
+
Nothing was destroyed, so there is nothing to refuse over. The boot reports
|
|
3081
|
+
instead:
|
|
3082
|
+
|
|
3083
|
+
```
|
|
3084
|
+
staged data-import leftovers:
|
|
3085
|
+
- 3 staging table(s) from a `replace` over api, last active 74 minute(s) ago — DROPPED: the run
|
|
3086
|
+
is not resumable and has been silent long enough that nothing is loading into them.
|
|
3087
|
+
The target of a staged `replace` is untouched until one short swap at the end, so none of this is
|
|
3088
|
+
a reason to refuse the boot — it is a reason to know the disk is holding a copy of a bundle.
|
|
3089
|
+
```
|
|
3090
|
+
|
|
3091
|
+
The run refreshes a heartbeat on that record every couple of seconds while rows
|
|
3092
|
+
land, which is what lets a boot tell the three cases apart:
|
|
3093
|
+
|
|
3094
|
+
| what the record says | what the boot does |
|
|
3095
|
+
|---|---|
|
|
3096
|
+
| silent past the threshold, started without `--no-atomic` | **drops** the tables it names |
|
|
3097
|
+
| still beating | leaves them — an import is loading into them right now, here or on another replica |
|
|
3098
|
+
| started `--no-atomic` | leaves them — its staging IS the resume point |
|
|
3099
|
+
|
|
3100
|
+
The threshold is `30` minutes by default. It is deliberately generous: the cost
|
|
3101
|
+
of collecting too early is that an in-flight import's swap fails with a missing
|
|
3102
|
+
table and you re-run it — the target is untouched either way — but the cost is
|
|
3103
|
+
still a re-run.
|
|
3104
|
+
|
|
3105
|
+
Declare a different one for a deployment whose imports routinely pause longer
|
|
3106
|
+
than that, waiting on an upstream export or a maintenance window:
|
|
3107
|
+
|
|
3108
|
+
```ts
|
|
3109
|
+
// app.config.ts
|
|
3110
|
+
export default {
|
|
3111
|
+
dataTransfer: {
|
|
3112
|
+
stagingStaleMinutes: 90,
|
|
3113
|
+
},
|
|
3114
|
+
}
|
|
3115
|
+
```
|
|
3116
|
+
|
|
3117
|
+
`VOLTRO_STAGING_STALE_MINUTES` overrides the declaration in turn — an operator
|
|
3118
|
+
acting on a running deployment outranks what the project declared. Note that the
|
|
3119
|
+
threshold decides only what a boot DROPS: leftover staging tables are named in
|
|
3120
|
+
the boot log either way.
|
|
3121
|
+
|
|
3122
|
+
What the boot does NOT collect, you can:
|
|
2975
3123
|
|
|
2976
3124
|
```bash
|
|
2977
3125
|
voltro data clear-staging --yes
|
|
2978
3126
|
```
|
|
2979
3127
|
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3128
|
+
It now labels each table with what its own run says, so a resume point is
|
|
3129
|
+
distinguishable from a leftover before you drop it:
|
|
3130
|
+
|
|
3131
|
+
```
|
|
3132
|
+
2 staging table(s) from an earlier `--mode replace`:
|
|
3133
|
+
_voltro_staging_tasks — RESUMABLE: a `--no-atomic` re-run continues from it, last active 4 min ago
|
|
3134
|
+
_voltro_staging_notes — no run claims it (an orphan, or from before the marker)
|
|
3135
|
+
```
|
|
2983
3136
|
|
|
2984
3137
|
A **cycle** in the bundle's foreign keys is detected before the load, not after
|
|
2985
3138
|
it. The swap inserts parents first, so two tables referencing each other cannot
|
|
@@ -3562,7 +3715,14 @@ A native run reports **blobs**, not rows: the vendor tool reports no row count w
|
|
|
3562
3715
|
|
|
3563
3716
|
### The provenance stamp — a restore that refuses the wrong DB
|
|
3564
3717
|
|
|
3565
|
-
A native dump is opaque: it doesn't say which dialect made it, which schema shape it carries, or when. `backup` writes a sidecar `voltro-backup-stamp.json` next to the artifact recording exactly that — `dialect`, the
|
|
3718
|
+
A native dump is opaque: it doesn't say which dialect made it, which schema shape it carries, or when. `backup` writes a sidecar `voltro-backup-stamp.json` next to the artifact recording exactly that — `dialect`, the `@voltro/cli` version, the timestamp, and **two** schema fingerprints.
|
|
3719
|
+
|
|
3720
|
+
Two, because they are different facts and only one of them is a claim about the artifact:
|
|
3721
|
+
|
|
3722
|
+
- **`schemaFingerprint`** — the source database's whole live schema at backup time. This is what the skew warning below compares against a target.
|
|
3723
|
+
- **`dumpFingerprint`** — the schema the **artifact carries**: that same snapshot minus the tables the dump excludes. On postgres and the mysql family those are `_voltro_replace_in_progress` and `_voltro_data_transfers` (see above); on sqlite, turso and mssql nothing is excluded and the two values are equal.
|
|
3724
|
+
|
|
3725
|
+
The distinction is not bookkeeping. `voltro data backup` opens its own run row in `_voltro_data_transfers` *before* it dumps, so on any database the framework has run against, the artifact is two tables short of the live schema it was taken from. Anything comparing a restored schema against a stamped one has to compare against `dumpFingerprint` — the drill did not, and failed every healthy backup with *"the artifact is inconsistent."*
|
|
3566
3726
|
|
|
3567
3727
|
`restore` reads the stamp **before touching the DB** and acts on two failures that are otherwise silent until they corrupt:
|
|
3568
3728
|
|
|
@@ -3578,13 +3738,39 @@ voltro data restore ./backups/2026-07-01 --drill --drill-url postgres://…/scra
|
|
|
3578
3738
|
# or set DRILL_DB_URL and just: voltro data restore ./backups/2026-07-01 --drill
|
|
3579
3739
|
```
|
|
3580
3740
|
|
|
3581
|
-
`--drill` restores the artifact into a **throwaway** database (from `--drill-url` / `DRILL_DB_URL`) and verifies it — **without ever touching the live DB**. It refuses a drill target that resolves to your live connection (a drill that `--clean`s production is the disaster it exists to rehearse against). After the restore it introspects the throwaway DB and
|
|
3741
|
+
`--drill` restores the artifact into a **throwaway** database (from `--drill-url` / `DRILL_DB_URL`) and verifies it — **without ever touching the live DB**. It refuses a drill target that resolves to your live connection (a drill that `--clean`s production is the disaster it exists to rehearse against). After the restore it introspects the throwaway DB and probes its migration ledger:
|
|
3582
3742
|
|
|
3583
3743
|
- **zero tables restored** → FAIL (the dump is empty or unreadable — this backup would not recover you),
|
|
3584
|
-
- **fingerprint disagrees with the stamp
|
|
3585
|
-
-
|
|
3744
|
+
- **schema fingerprint disagrees with the stamp's `dumpFingerprint`** → FAIL (the restore didn't reproduce what was backed up),
|
|
3745
|
+
- **`_voltro_migration_plans` restored EMPTY** → FAIL (see below),
|
|
3746
|
+
- **tables + matching fingerprint + a populated or absent ledger** → PASS.
|
|
3747
|
+
|
|
3748
|
+
A stamp too old to carry a `dumpFingerprint` gives a **PASS (partial)** that says the shape could not be cross-checked. It does not fall back to `schemaFingerprint`: that is the comparison that fails a healthy backup, and a check that is red on every real input gets switched off — taking its genuine failures with it.
|
|
3749
|
+
|
|
3750
|
+
It exits non-zero on any FAIL, so a scheduled CI job turns a silently-broken backup into a red build. Run it against your latest artifact on a cron — a backup you've never restored is a hypothesis, and this is how you keep it a fact.
|
|
3751
|
+
|
|
3752
|
+
#### The ledger check — the one thing a schema comparison cannot see
|
|
3753
|
+
|
|
3754
|
+
A fingerprint answers *"is the shape right?"*. A drill's real question is *"would my app come up against this?"*, and the gap between them is **content** — a framework table that restored with the right columns and the wrong rows.
|
|
3586
3755
|
|
|
3587
|
-
|
|
3756
|
+
`voltro serve`'s boot gate reads the newest row of `_voltro_migration_plans` and refuses with `prod-mismatch` when there is none. So a ledger table that restores with exactly the right columns and **zero rows** is a database no source tree can boot, and its schema fingerprint is identical to a healthy one's. The drill fails that, and names it:
|
|
3757
|
+
|
|
3758
|
+
```
|
|
3759
|
+
FAIL — restored 30 table(s) with the right shape, but `_voltro_migration_plans`
|
|
3760
|
+
came back EMPTY.
|
|
3761
|
+
`voltro serve` reads the newest row of that table as its boot gate and
|
|
3762
|
+
refuses with `prod-mismatch` when there is none.
|
|
3763
|
+
```
|
|
3764
|
+
|
|
3765
|
+
A restored database with **no ledger table at all** is not a voltro-managed schema (a hand-made dump, someone else's database) — the drill says so and claims nothing about booting it, rather than failing it.
|
|
3766
|
+
|
|
3767
|
+
What the drill deliberately does **not** judge is a ledger whose fingerprint differs from what your code declares. It has no way to know which commit you will deploy next to this database, and `voltro db apply` clears that state anyway; failing a backup for it would make the drill red for a reason that is not about the backup.
|
|
3768
|
+
|
|
3769
|
+
#### Why there is no full app boot
|
|
3770
|
+
|
|
3771
|
+
Booting a real app against the restored database sounds like the stronger check, and it would be a weaker one. There is no app in the drill's path — it would have to boot a **fixture**, and a fixture booting says nothing about whether *your* app boots. It moves the drill from *"proves your backup"* to *"proves our fixture"* while reading as the bigger claim.
|
|
3772
|
+
|
|
3773
|
+
The part worth having does not need a process: the boot gate is a comparison, not a startup sequence, so the one boot-fatal condition that holds regardless of which code you deploy is reachable with a `SELECT`. That is the ledger check above.
|
|
3588
3774
|
|
|
3589
3775
|
### Point-in-time recovery (PITR) is your database's job, not the framework's
|
|
3590
3776
|
|
|
@@ -4465,6 +4651,25 @@ prints only because your tree matched it.
|
|
|
4465
4651
|
|
|
4466
4652
|
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).
|
|
4467
4653
|
|
|
4654
|
+
### When nothing changed, read WHICH nothing
|
|
4655
|
+
|
|
4656
|
+
Two states end an update with no diff, and they mean opposite things: the jump
|
|
4657
|
+
ships no codemods, or it ships some and every one of them decided your project
|
|
4658
|
+
is not affected. The summary names which:
|
|
4659
|
+
|
|
4660
|
+
```text
|
|
4661
|
+
codemods: 2 ship for this jump; none matched your project.
|
|
4662
|
+
· 0.55.0/01_target-relations-declare-columns — its own check found nothing to change
|
|
4663
|
+
· 0.55.0/02_widget-kind-gained-rich-text — its own check found nothing to change
|
|
4664
|
+
```
|
|
4665
|
+
|
|
4666
|
+
`none ship for this jump` is the first. The second lists the ids, because a
|
|
4667
|
+
codemod's own check is a predicate that can be wrong — and a gate that reads
|
|
4668
|
+
the wrong files answers "does not apply" for a project that is fully affected.
|
|
4669
|
+
If you recognise a subject in that list as something your app *does* use, that
|
|
4670
|
+
is a bug in the check rather than a fact about your code; the CHANGELOG entry
|
|
4671
|
+
for the version says what each one looks for.
|
|
4672
|
+
|
|
4468
4673
|
## The database is separate
|
|
4469
4674
|
|
|
4470
4675
|
`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.
|