@voltro/cli 0.58.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/apiBuild-B9OvhITV.js +2 -0
  3. package/dist/{apiBuild-CkpsCqIu.js → apiBuild-BU5cd-Fn.js} +50 -48
  4. package/dist/bin.js +1 -1
  5. package/dist/{build-51qBWfYY.js → build-uRMZrT8z.js} +338 -253
  6. package/dist/{checkCommand-C1EbNmwl.js → checkCommand-C1CQc8lD.js} +1 -1
  7. package/dist/{checkCommand-C30J2F18.js → checkCommand-DC7k1_hP.js} +1 -1
  8. package/dist/{codemodRunner-BKjCwGiN.js → codemodRunner-kNnh1I7_.js} +474 -390
  9. package/dist/{commands-DowWkYtd.js → commands-cu34ENUT.js} +10 -10
  10. package/dist/{doctorCommand-5ULMpL10.js → doctorCommand-CGjefPmk.js} +2 -2
  11. package/dist/doctorCommand-DXnNpKQT.js +2 -0
  12. package/dist/{evolveCommand-DTawtTxf.js → evolveCommand-8CuFBlbQ.js} +2 -2
  13. package/dist/index.js +1 -1
  14. package/dist/{infoCommand-BFksHunl.js → infoCommand-D02A4WI2.js} +1 -1
  15. package/dist/{runtimeTrace-BUk5zMXY.js → runtimeTrace-DDuKJgVi.js} +1 -1
  16. package/dist/{sdkgen-CAhmmbjx.js → sdkgen-vq1fF9D_.js} +1 -1
  17. package/dist/serveCommand-1R52mo6F.js +2 -0
  18. package/dist/{serveCommand-DhtC7WBq.js → serveCommand-Dr4KXmTS.js} +1 -1
  19. package/dist/serveEntry.js +1 -1
  20. package/dist/{updateCommand-CtuD5r1t.js → updateCommand-Bw3mCUDT.js} +1 -1
  21. package/dist/updateCommand-inInBIAh.js +2 -0
  22. package/package.json +31 -19
  23. package/templates/AGENTS.md +1 -1
  24. package/templates/agent-docs/_index.md +1 -1
  25. package/templates/agent-docs/ai.md +50 -1
  26. package/templates/agent-docs/cli.md +7 -6
  27. package/templates/agent-docs/plugins/sentry.md +54 -15
  28. package/templates/agent-docs/whats-new.md +24 -170
  29. package/templates/apps/api-ai/package.json +6 -6
  30. package/templates/apps/api-auth/package.json +8 -8
  31. package/templates/apps/api-backend/package.json +7 -7
  32. package/templates/apps/api-backend-deactivation/package.json +7 -7
  33. package/templates/apps/api-backend-mail/package.json +8 -8
  34. package/templates/apps/api-backend-mariadb/package.json +9 -9
  35. package/templates/apps/api-backend-sqlite/package.json +8 -8
  36. package/templates/apps/api-backend-storage/package.json +8 -8
  37. package/templates/apps/api-cms/package.json +9 -9
  38. package/templates/apps/api-collab/package.json +8 -8
  39. package/templates/apps/api-data-advanced/package.json +8 -8
  40. package/templates/apps/api-durable/package.json +8 -8
  41. package/templates/apps/api-feature-flags/package.json +9 -9
  42. package/templates/apps/api-governance/package.json +8 -8
  43. package/templates/apps/api-kv/package.json +8 -8
  44. package/templates/apps/api-moderation/package.json +8 -8
  45. package/templates/apps/api-observability/package.json +8 -8
  46. package/templates/apps/api-ratelimit/package.json +8 -8
  47. package/templates/apps/api-rbac/package.json +8 -8
  48. package/templates/apps/api-rest/package.json +7 -7
  49. package/templates/apps/api-row-history/package.json +8 -8
  50. package/templates/apps/api-saas/package.json +11 -11
  51. package/templates/apps/api-saas-starter/package.json +10 -10
  52. package/templates/apps/api-search/package.json +8 -8
  53. package/templates/apps/api-status/package.json +8 -8
  54. package/templates/apps/api-webhooks/package.json +9 -9
  55. package/templates/apps/changelog/package.json +7 -7
  56. package/templates/apps/edge-functions/package.json +2 -2
  57. package/templates/apps/frontend-admin/package.json +7 -7
  58. package/templates/apps/frontend-app/package.json +8 -8
  59. package/templates/apps/frontend-auth/package.json +7 -7
  60. package/templates/apps/frontend-blank/package.json +6 -6
  61. package/templates/apps/frontend-cms/package.json +8 -8
  62. package/templates/apps/frontend-collab/package.json +9 -9
  63. package/templates/apps/frontend-contact/package.json +7 -7
  64. package/templates/apps/frontend-dashboard/package.json +6 -6
  65. package/templates/apps/frontend-docs/package.json +8 -8
  66. package/templates/apps/frontend-i18n/package.json +6 -6
  67. package/templates/apps/frontend-landing/package.json +7 -7
  68. package/templates/apps/frontend-portal/package.json +7 -7
  69. package/templates/apps/frontend-saas/package.json +7 -7
  70. package/templates/apps/frontend-spa/package.json +6 -6
  71. package/templates/apps/frontend-ssr/package.json +6 -6
  72. package/templates/apps/frontend-ssr-api/package.json +7 -7
  73. package/templates/apps/frontend-static-blog/package.json +8 -8
  74. package/templates/apps/frontend-status/package.json +7 -7
  75. package/templates/apps/mobile-app/package.json +4 -4
  76. package/dist/apiBuild-CDE4NXNi.js +0 -2
  77. package/dist/doctorCommand-B8H2MYCE.js +0 -2
  78. package/dist/serveCommand-BUXUtSF5.js +0 -2
  79. package/dist/updateCommand-eVKVHj3N.js +0 -2
@@ -121,7 +121,7 @@ var u = () => {
121
121
  name: "check",
122
122
  summary: "Verify the app graph (blast-radius checks) against a running api; LLM-shaped --json diagnostics. Flags: --json, --process <p>, --diff removeTable:<name>.",
123
123
  status: "wired",
124
- run: (e) => import("./checkCommand-C1EbNmwl.js").then((t) => t.runCheckCommand(e)),
124
+ run: (e) => import("./checkCommand-C1CQc8lD.js").then((t) => t.runCheckCommand(e)),
125
125
  help: {
126
126
  usage: "[--json] [--offline] [--process <name>] [--diff removeTable:<name>]",
127
127
  flags: [
@@ -171,7 +171,7 @@ var u = () => {
171
171
  name: "evolve",
172
172
  summary: "Schema-evolution copilot: propose an observed-graph-driven codemod + a branch-verified backfill for a schema change (rename/retype/split/drop a column, rename a table). Dry-run by default; --write applies. Usage: voltro evolve rename-column <table>.<col> --to <newName> [--write] [--json].",
173
173
  status: "wired",
174
- run: (e) => import("./evolveCommand-DTawtTxf.js").then((t) => t.runEvolveCommand(e)),
174
+ run: (e) => import("./evolveCommand-8CuFBlbQ.js").then((t) => t.runEvolveCommand(e)),
175
175
  help: {
176
176
  usage: "<rename-column|rename-table|retype-column|split-column|drop-column> <target> [--to <new>] [--write]",
177
177
  flags: [
@@ -281,7 +281,7 @@ var u = () => {
281
281
  name: "info",
282
282
  summary: "Report versions (CLI, node, every installed @voltro/* — flags lockstep skew), package manager, and dialect. --json for CI. Exits 1 on version skew.",
283
283
  status: "wired",
284
- run: (e) => import("./infoCommand-BFksHunl.js").then((t) => t.runInfoCommand(e)),
284
+ run: (e) => import("./infoCommand-D02A4WI2.js").then((t) => t.runInfoCommand(e)),
285
285
  help: {
286
286
  usage: "[--json]",
287
287
  flags: [["--json", "machine-readable report"]],
@@ -331,12 +331,12 @@ var u = () => {
331
331
  run: async (e) => {
332
332
  let t = e.find((e) => !e.startsWith("-")) ?? process.cwd();
333
333
  if (e.includes("--preflight")) {
334
- let { runServePreflight: e } = await import("./doctorCommand-B8H2MYCE.js");
334
+ let { runServePreflight: e } = await import("./doctorCommand-DXnNpKQT.js");
335
335
  return e(s(t), !0);
336
336
  }
337
337
  let { tryRunWebServe: n } = await import("./webDev-Bzb8ZFus.js"), r = await n(s(t));
338
338
  if (!r.ran) {
339
- let { runServeDispatch: t } = await import("./serveCommand-BUXUtSF5.js");
339
+ let { runServeDispatch: t } = await import("./serveCommand-1R52mo6F.js");
340
340
  return t(e);
341
341
  }
342
342
  return r.exitCode;
@@ -364,13 +364,13 @@ var u = () => {
364
364
  handlesHelp: !0,
365
365
  summary: "Preflight an app for production: the precompiled serve bundle, hand-rolled primitives (naming the shipped one), @voltro/* version skew, executors with no access check, `.serverOnly()` leaks, and junction-FK gaps.",
366
366
  status: "wired",
367
- run: (e) => import("./doctorCommand-B8H2MYCE.js").then((t) => t.runDoctorCommand(e))
367
+ run: (e) => import("./doctorCommand-DXnNpKQT.js").then((t) => t.runDoctorCommand(e))
368
368
  },
369
369
  {
370
370
  name: "build",
371
371
  summary: "Production build for a web app (vite build + per-page SSG pre-render).",
372
372
  status: "wired",
373
- run: (e) => import("./build-51qBWfYY.js").then((t) => t.runBuildCommand(e)),
373
+ run: (e) => import("./build-uRMZrT8z.js").then((t) => t.runBuildCommand(e)),
374
374
  help: {
375
375
  usage: "[app] [--target <t>]",
376
376
  flags: [["--target <t>", "build target"], ["--report", "print what a visitor downloads: first load, chunks, server artefacts"]],
@@ -386,7 +386,7 @@ var u = () => {
386
386
  name: "prune-runtime",
387
387
  summary: "Trim a deployed web tree’s node_modules to the reachable runtime set (standalone image slimming).",
388
388
  status: "wired",
389
- run: (e) => import("./runtimeTrace-BUk5zMXY.js").then((t) => t.runPruneRuntimeCommand(e)),
389
+ run: (e) => import("./runtimeTrace-DDuKJgVi.js").then((t) => t.runPruneRuntimeCommand(e)),
390
390
  hidden: !0
391
391
  },
392
392
  {
@@ -555,14 +555,14 @@ var u = () => {
555
555
  handlesHelp: !0,
556
556
  summary: "Upgrade the app to the latest framework: bump every @voltro/* dependency (across ALL workspace members, install once at the workspace root), then run the codemods shipped with the target version. Flags: --to <version>, --from <version>, --codemods-only, --dry-run, --force, --exact, --help. After: run `voltro db apply`.",
557
557
  status: "wired",
558
- run: (e) => import("./updateCommand-eVKVHj3N.js").then((t) => t.runUpdateCommand(e))
558
+ run: (e) => import("./updateCommand-inInBIAh.js").then((t) => t.runUpdateCommand(e))
559
559
  },
560
560
  {
561
561
  name: "_apply-codemods",
562
562
  summary: "Internal: apply shipped codemods for a framework version jump (re-exec target of `voltro update`).",
563
563
  status: "wired",
564
564
  hidden: !0,
565
- run: (e) => import("./updateCommand-eVKVHj3N.js").then((t) => t.runApplyCodemods(e))
565
+ run: (e) => import("./updateCommand-inInBIAh.js").then((t) => t.runApplyCodemods(e))
566
566
  },
567
567
  {
568
568
  name: "embeddings",
@@ -1,6 +1,6 @@
1
1
  import { Xt as e, Yt as t, Zt as n } from "./dev-Ce2gEziv.js";
2
2
  import { Bt as r, Ht as i, T as a, Ut as o, V as s, Vt as c, t as l, u } from "./webDev-CBN9KkyQ.js";
3
- import { r as d, u as f } from "./apiBuild-CkpsCqIu.js";
3
+ import { r as d, u as f } from "./apiBuild-BU5cd-Fn.js";
4
4
  import { S as p } from "./frameworkTableAssembly-vfkzuzEo.js";
5
5
  import { n as m } from "./pageConvention-CMpfDN6r.js";
6
6
  import { n as h, r as g, t as ee } from "./cliOutput-D1tSBoRM.js";
@@ -2162,7 +2162,7 @@ var le = {
2162
2162
  findings: []
2163
2163
  });
2164
2164
  try {
2165
- let { offlineManifest: n, toInput: r } = await import("./checkCommand-C1EbNmwl.js"), { buildAppGraph: i, checkAppGraph: a } = await import("./appGraph-CFmFKHb2.js"), o = await n(e), s = r(o.manifest, o.tables), c = await zn(e), l = [.../* @__PURE__ */ new Set([...s.scopes ?? [], ...c])];
2165
+ let { offlineManifest: n, toInput: r } = await import("./checkCommand-C1CQc8lD.js"), { buildAppGraph: i, checkAppGraph: a } = await import("./appGraph-CFmFKHb2.js"), o = await n(e), s = r(o.manifest, o.tables), c = await zn(e), l = [.../* @__PURE__ */ new Set([...s.scopes ?? [], ...c])];
2166
2166
  if (l.length === 0) {
2167
2167
  try {
2168
2168
  let { loadAppSchemaInputs: t } = await import("./frameworkTableAssembly-B96WCNJA.js");
@@ -0,0 +1,2 @@
1
+ import { b as e, y as t } from "./doctorCommand-CGjefPmk.js";
2
+ export { t as runDoctorCommand, e as runServePreflight };
@@ -1,6 +1,6 @@
1
1
  import { n as e } from "./cliOutput-D1tSBoRM.js";
2
- import { i as t } from "./checkCommand-C30J2F18.js";
3
- import { i as n, n as r } from "./codemodRunner-BKjCwGiN.js";
2
+ import { i as t } from "./checkCommand-DC7k1_hP.js";
3
+ import { i as n, n as r } from "./codemodRunner-kNnh1I7_.js";
4
4
  import { basename as i, join as a, resolve as o } from "node:path";
5
5
  import { branchNamespaceName as s, planBranchProvision as c } from "@voltro/database";
6
6
  import { readFile as l } from "node:fs/promises";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, i as t, n, o as r, r as i, t as a } from "./commands-DowWkYtd.js";
1
+ import { a as e, i as t, n, o as r, r as i, t as a } from "./commands-cu34ENUT.js";
2
2
  import { r as o } from "./templates-BTWZkJJT.js";
3
3
  //#region src/index.ts
4
4
  var s = "framework";
@@ -1,5 +1,5 @@
1
1
  import { n as e, r as t, t as n } from "./cliOutput-D1tSBoRM.js";
2
- import { o as r } from "./updateCommand-CtuD5r1t.js";
2
+ import { o as r } from "./updateCommand-Bw3mCUDT.js";
3
3
  import { join as i, resolve as a } from "node:path";
4
4
  import { readFileSync as o, readdirSync as s, statSync as c } from "node:fs";
5
5
  //#region src/infoCommand.ts
@@ -1,4 +1,4 @@
1
- import { n as e } from "./apiBuild-CkpsCqIu.js";
1
+ import { n as e } from "./apiBuild-BU5cd-Fn.js";
2
2
  import { join as t, resolve as n, sep as r } from "node:path";
3
3
  import { existsSync as i, promises as a } from "node:fs";
4
4
  import { createLogger as o } from "@voltro/logger";
@@ -422,7 +422,7 @@ let package = Package(
422
422
  await l.mkdir(o(e), { recursive: !0 }), await l.writeFile(e, n.content, "utf8");
423
423
  }
424
424
  }, X = async (e) => {
425
- let { offlineManifest: t } = await import("./checkCommand-C1EbNmwl.js"), { manifest: n } = await t(e);
425
+ let { offlineManifest: t } = await import("./checkCommand-C1CQc8lD.js"), { manifest: n } = await t(e);
426
426
  return n;
427
427
  }, Z = async (e) => {
428
428
  let t = J(await (e.manifestProvider ?? X)(e.root), {
@@ -0,0 +1,2 @@
1
+ import { n as e } from "./serveCommand-Dr4KXmTS.js";
2
+ export { e as runServeDispatch };
@@ -9,7 +9,7 @@ import { Yt as sn, en as cn, ft as ln, ht as un, lt as dn, mt as fn, pt as pn, q
9
9
  import { g as hn, s as gn } from "./inspect-PcC5J-Dl.js";
10
10
  import { n as _n } from "./cliArgs-D4p8n7EE.js";
11
11
  import { r as vn } from "./appPort-B_HpJ_ck.js";
12
- import { r as yn, u as bn } from "./apiBuild-CkpsCqIu.js";
12
+ import { r as yn, u as bn } from "./apiBuild-BU5cd-Fn.js";
13
13
  import { r as xn } from "./dialectDriver-czCHYpeH.js";
14
14
  import { i as Sn, n as Cn } from "./connectionConfig-Bk9IC7D0.js";
15
15
  import { c as wn, n as Tn, o as En } from "./seedRunner-DISBKow-.js";
@@ -1,4 +1,4 @@
1
- import { t as e } from "./serveCommand-DhtC7WBq.js";
1
+ import { t as e } from "./serveCommand-Dr4KXmTS.js";
2
2
  import { t } from "./loadEnv-D9nEOClM.js";
3
3
  import { r as n } from "./appModuleLoader-C9r9mxZt.js";
4
4
  import { i as r } from "./dialectDriver-czCHYpeH.js";
@@ -1,4 +1,4 @@
1
- import { n as e, r as t, t as n } from "./codemodRunner-BKjCwGiN.js";
1
+ import { n as e, r as t, t as n } from "./codemodRunner-kNnh1I7_.js";
2
2
  import { basename as r, dirname as i, join as a, relative as o, resolve as s } from "node:path";
3
3
  import { existsSync as c, readFileSync as l, readdirSync as u, statSync as d, writeFileSync as f } from "node:fs";
4
4
  import { totalmem as p } from "node:os";
@@ -0,0 +1,2 @@
1
+ import { m as e, p as t } from "./updateCommand-Bw3mCUDT.js";
2
+ export { t as runApplyCodemods, e as runUpdateCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.58.0",
3
+ "version": "0.59.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",
@@ -844,6 +844,18 @@
844
844
  "title": "rerank reaches the gateway through a ProviderConfig",
845
845
  "kind": "manual"
846
846
  },
847
+ {
848
+ "version": "0.59.0",
849
+ "id": "0.59.0/01_sourcemaps_moved_to_the_top_level",
850
+ "title": "`web.sourcemaps` moved to the top level of app.config.ts",
851
+ "kind": "transform"
852
+ },
853
+ {
854
+ "version": "0.59.0",
855
+ "id": "0.59.0/02_costsource_gained_unpriced",
856
+ "title": "`costSource` gained `unpriced`, and AI usage is counted in a unit",
857
+ "kind": "manual"
858
+ },
847
859
  {
848
860
  "version": "0.6.0",
849
861
  "id": "0.6.0/01_no-dev-session-secret",
@@ -926,24 +938,24 @@
926
938
  "@effect/platform-node": "^0.108.0",
927
939
  "@effect/sql": "^0.52.0",
928
940
  "@effect/workflow": "^0.19.0",
929
- "@voltro/ai": "0.58.0",
930
- "@voltro/cache": "0.58.0",
931
- "@voltro/client": "0.58.0",
932
- "@voltro/content": "0.58.0",
933
- "@voltro/data-transfer": "0.58.0",
934
- "@voltro/database": "0.58.0",
935
- "@voltro/env": "0.58.0",
936
- "@voltro/kv": "0.58.0",
937
- "@voltro/logger": "0.58.0",
938
- "@voltro/plugin-auth": "0.58.0",
939
- "@voltro/plugin-broadcast": "0.58.0",
940
- "@voltro/plugin-mail": "0.58.0",
941
- "@voltro/plugin-storage": "0.58.0",
942
- "@voltro/plugin-webhooks": "0.58.0",
943
- "@voltro/protocol": "0.58.0",
944
- "@voltro/runtime": "0.58.0",
945
- "@voltro/serverless": "0.58.0",
946
- "@voltro/workflow": "0.58.0",
941
+ "@voltro/ai": "0.59.0",
942
+ "@voltro/cache": "0.59.0",
943
+ "@voltro/client": "0.59.0",
944
+ "@voltro/content": "0.59.0",
945
+ "@voltro/data-transfer": "0.59.0",
946
+ "@voltro/database": "0.59.0",
947
+ "@voltro/env": "0.59.0",
948
+ "@voltro/kv": "0.59.0",
949
+ "@voltro/logger": "0.59.0",
950
+ "@voltro/plugin-auth": "0.59.0",
951
+ "@voltro/plugin-broadcast": "0.59.0",
952
+ "@voltro/plugin-mail": "0.59.0",
953
+ "@voltro/plugin-storage": "0.59.0",
954
+ "@voltro/plugin-webhooks": "0.59.0",
955
+ "@voltro/protocol": "0.59.0",
956
+ "@voltro/runtime": "0.59.0",
957
+ "@voltro/serverless": "0.59.0",
958
+ "@voltro/workflow": "0.59.0",
947
959
  "chokidar": "^5.0.0",
948
960
  "ioredis": "^5.11.1",
949
961
  "tinyglobby": "^0.2.17",
@@ -749,7 +749,7 @@ each plugin's own README.
749
749
 
750
750
  | Topic | Open | Summary |
751
751
  |---|---|---|
752
- | **What's new in 0.58.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. |
752
+ | **What's new in 0.59.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. |
753
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. |
754
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. |
755
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.58.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
12
+ | **What's new in 0.59.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. |
@@ -185,6 +185,20 @@ const usable = models.filter(canExecute).filter((m) => m.capabilities?.vision)
185
185
 
186
186
  Each entry adds `capabilities`, `modalities`, `contextWindow`, `maxOutputTokens`, `releasedAt`, `knowledgeCutoff` and `dataPolicy`.
187
187
 
188
+ Plus **`raw`** — the entry exactly as it came over the wire.
189
+
190
+ The normalised fields are the shared answer to the questions every app on the gateway asks. `raw` is for the ones only one app asks: a chips row built from the vendor's own `tags`, a pricing display that shows a structure we have not normalised, `zdr` and `no_training` kept apart instead of folded into `dataPolicy`.
191
+
192
+ ```ts
193
+ const models = await readGatewayCatalog({ modality: 'language' })
194
+ const chips = models[0]?.raw?.tags // vendor's own vocabulary
195
+ const audioRate = (models[0]?.raw?.pricing as { audio_per_second?: string })?.audio_per_second
196
+ ```
197
+
198
+ Without it, an app with its own display fetches `/v1/models` a second time and keeps a second parser of the same response — and then has to decide again, at every field the gateway adds, whether to wait for a framework release. That is the decision `raw` removes, which is why it is one field rather than three more normalised ones.
199
+
200
+ It is absent on the injected-provider path (the seam a test uses), for the same reason `capabilities` is: that path never saw a response, and an empty object would turn "no wire data" into the claim "the wire carried nothing".
201
+
188
202
  `capabilities` is **unioned from three fields that each half-say it** — `tags`, `supported_parameters` and `modalities` — because they overlap and regularly disagree: a model tagged `reasoning` does not always carry the parameter, and the other way round. Taking either alone produces a false negative, and a false "cannot" removes a model from a picker with no way for anyone to find out why. That union is framework knowledge; without it every app writes the same forty lines, differently.
189
203
 
190
204
  `dataPolicy` carries `zeroRetention` and `noTraining` (`'all' | 'some' | 'none'`). They decide whether a model may see customer text, and an unreadable value reads as `'none'` — the question is "may this", and an unparseable answer is not a yes.
@@ -2589,7 +2603,42 @@ yield* generateSpeech({ text, provider: { name: 'elevenlabs', model: 'eleven_tur
2589
2603
 
2590
2604
  This ships **no** adapters, on purpose: bundling an ElevenLabs client would put the framework in the business of tracking somebody else's REST API, and the app already has the adapter. What the app deletes is its dispatch, not its adapters.
2591
2605
 
2592
- A registered provider receives every option the primitive already had — `voice`, `outputFormat`, `speed`, `language`, `instructions`, `providerOptions` — or the seam would be a narrower primitive wearing the same name. Built-in names (`mock`, `openai`, `gateway`, `anthropic`) cannot be shadowed: silently overriding one would make the same config mean different things depending on which module loaded first.
2606
+ A registered provider receives every option the primitive already had — `voice`, `outputFormat`, `speed`, `language`, `instructions`, `providerOptions` — or the seam would be a narrower primitive wearing the same name.
2607
+
2608
+ #### Taking over a built-in name
2609
+
2610
+ `mock`, `openai`, `gateway` and `anthropic` resolve without a registration, and shadowing one by accident would make the same config mean different things depending on which module loaded first. So it takes a flag:
2611
+
2612
+ ```ts
2613
+ registerSpeechProvider('openai', myOpenAiTts, { replaceBuiltin: true })
2614
+ ```
2615
+
2616
+ The flag exists because a provider name is not always an alias you choose. `openai` is the value the gateway puts in its model ids, so it is what a stored catalog's rows carry and what a picker submits — an app told to pick another name cannot, without rewriting persisted rows and live sessions. Refusing outright did not protect that app; it made it keep its own dispatch for every provider, not just the colliding one.
2617
+
2618
+ What the refusal was protecting is kept: the intent is now in the call, so an override is a decision someone wrote down rather than an accident of load order. Registered providers are resolved **before** built-ins — otherwise the flag would let you register a name and never reach it.
2619
+
2620
+ ### Cost — speech is not billed in tokens
2621
+
2622
+ `generateSpeech` never records a ledger row itself; it returns `usage` and you pass it to `recordAiUsage`, the same as `generateText`. The difference is the unit. Every speech vendor meters characters of input or seconds of audio, so a `usage` shape that only knew tokens could not hold the call at all — speech spend was not un-priced, it was **absent**.
2623
+
2624
+ A registered provider returns a `MeteredUsage`:
2625
+
2626
+ ```ts
2627
+ synthesize: (request) => Effect.succeed({
2628
+ audio,
2629
+ usage: { unit: 'characters', input: request.text.length },
2630
+ })
2631
+ ```
2632
+
2633
+ ```ts
2634
+ yield* recordAiUsage(store, {
2635
+ provider: 'elevenlabs', model: 'eleven_turbo_v3', operation: 'generateSpeech',
2636
+ usage: { unit: 'characters', input: 4200 },
2637
+ price: { inputPer1M: 30, outputPer1M: 0 }, // your rate, in your unit
2638
+ })
2639
+ ```
2640
+
2641
+ The ledger row carries `unit` beside the two counts. **A non-token unit is never priced by the token table**: without a `price` or an `actualCostUsd` the row is written `costSource: 'unpriced'` with the real count and a zero cost, because a per-1M-token rate applied to a character count produces a number indistinguishable from a real one. Sum the count columns grouped by `unit`, never across it.
2593
2642
 
2594
2643
  ## As a durable step — `transcribeStep`
2595
2644
 
@@ -1455,13 +1455,14 @@ web: {
1455
1455
  // `dist/assets/` to the prefix on deploy. Filenames are content-hashed, so a
1456
1456
  // previous deploy's assets stay valid for a visitor mid-navigation.
1457
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
1458
  },
1459
+
1460
+ // Top level, NOT under `web:` — it covers every bundle the build minifies
1461
+ // (browser, SSR, start, and an api app's serve bundle). Emits `.map` files with
1462
+ // no `//# sourceMappingURL` comment, so nothing shipped points at them.
1463
+ // `plugin-sentry` can upload and sweep them for you; `voltro start` refuses to
1464
+ // serve a `.map` regardless, so a forgotten delete is not a leak.
1465
+ sourcemaps: 'hidden',
1465
1466
  http: {
1466
1467
  // Node hangs up an idle keep-alive connection after 5s; every proxy in front
1467
1468
  // holds one longer, and the request that lands in that window comes back as
@@ -161,17 +161,15 @@ The two halves are independent: the api plugin alone gives you server traces + e
161
161
 
162
162
  **Traces default to on, at `tracesSampleRate: 1.0`.** That is the coherent default rather than a generous one: the browser half already sampled every page load, navigation and rpc call at 1.0, so with the api half off each of those spans was emitted, paid for, and belonged to a trace with no server side — a browser hop hanging off nothing. The rate is named on the boot line (`sentry active traces=true tracesSampleRate=1`) so a first boot shows what it is about to send; lower it with `tracesSampleRate`, or set `traces: false` to keep errors and breadcrumbs only.
163
163
 
164
- ## Source maps (browser stack traces)
164
+ ## Source maps (every bundle, browser and server)
165
165
 
166
- The web bundle is minified, so browser errors arrive in Sentry with **minified** stack traces unless the maps are uploaded. `voltro build` does it:
166
+ Every bundle `voltro build` emits is minified, so an error arrives in Sentry with a **minified** stack trace unless the maps are uploaded. `voltro build` does it:
167
167
 
168
168
  ```ts
169
- // app.config.ts
170
- web: {
171
- sourcemaps: {
172
- mode: 'hidden',
173
- upload: { org: 'acme', project: 'web' },
174
- },
169
+ // app.config.ts — top level, for a web app AND an api app
170
+ sourcemaps: {
171
+ mode: 'hidden',
172
+ upload: { org: 'acme', project: 'web' },
175
173
  }
176
174
  ```
177
175
 
@@ -191,11 +189,9 @@ That emits the maps, uploads them through `@sentry/cli`, and then **removes them
191
189
  `app.config.ts` is TypeScript, so the ordinary branch is the answer:
192
190
 
193
191
  ```ts
194
- web: {
195
- sourcemaps: process.env.CI
196
- ? { mode: 'hidden', upload: { org: 'acme', project: 'web' } }
197
- : 'hidden',
198
- }
192
+ sourcemaps: process.env.CI
193
+ ? { mode: 'hidden', upload: { org: 'acme', project: 'web' } }
194
+ : 'hidden',
199
195
  ```
200
196
 
201
197
  `'hidden'` alone emits the maps and **keeps** them — right for reading a stack trace locally, and a leak in an image. Omit the field entirely and nothing is emitted, which is the default.
@@ -206,9 +202,52 @@ web: {
206
202
 
207
203
  It is read from `SENTRY_AUTH_TOKEN` and there is deliberately no `authToken` option: `app.config.ts` is a committed file, and a token with `project:releases` scope can write to every project in the org. Give it to the **build** (a CI secret, a Docker build secret) — not to the deployment, since the upload happens while the bundle is being built.
208
204
 
209
- **Install `@sentry/cli` yourself: `pnpm add -D @sentry/cli` in the web app.** The framework depends on it nowhere, and that is a licence decision rather than a packaging one — it is FSL-1.1-MIT, which restricts competing commercial use, so it must not sit in the dependency graph of a package we publish. A build with an upload configured and the package missing **refuses and names the command**, rather than skipping the upload and leaving you to find out months later that no frame resolves.
205
+ **Install `@sentry/cli` yourself: `pnpm add -D @sentry/cli` in the app being built.** The framework depends on it nowhere, and that is a licence decision rather than a packaging one — it is FSL-1.1-MIT, which restricts competing commercial use, so it must not sit in the dependency graph of a package we publish. A build with an upload configured and the package missing **refuses and names the command**, rather than skipping the upload and leaving you to find out months later that no frame resolves.
206
+
207
+ ### It covers the server too — and that is what it was missing
208
+
209
+ An earlier version of this page said server traces were un-minified already and
210
+ that source maps were a browser-only concern. That was wrong about the built
211
+ artefact, and the correction is the reason the option moved to the top level.
212
+
213
+ `voltro build` minifies **four** bundles, not one: the browser bundle, the SSR
214
+ bundle, the `voltro start` bundle, and — for an api app — the precompiled entry
215
+ plus the serve bundle that `voltro serve` actually runs. `keepNames` rescues the
216
+ handful of identifiers that are read as strings (Effect tags, error `name`s) and
217
+ nothing else, so a frame out of the serve bundle reads:
218
+
219
+ ```
220
+ at l (file:///…/serveBundle/serveEntry.js:79:51)
221
+ at Qo (file:///…/serveBundle/serveEntry.js:236:76405)
222
+ ```
210
223
 
211
- Server stack traces are un-minified already (the api ships readable JS), so this is a browser-only concern.
224
+ The server half is also where it hurts most. A schedule, a workflow step, a
225
+ subscriber or a REST route has no browser caller whose frame could stand in for
226
+ it — a workflow that dies at 03:30 in a cron is only what its stack trace says.
227
+
228
+ So `sourcemaps` is one field at the top level of `app.config.ts` and it covers
229
+ everything the build minifies. **It replaces `web.sourcemaps`**, which reached
230
+ only the browser bundle; the build refuses the old spelling rather than ignoring
231
+ it, and `voltro update` moves it for you.
232
+
233
+ Two details worth knowing before the first upload:
234
+
235
+ - **Each output directory is uploaded under the prefix that matches how its
236
+ files are named.** `dist/assets` is fetched by a browser under `~/assets`; the
237
+ SSR, start and serve bundles are files a node process loaded and go up under
238
+ `~/`. Set `upload.urlPrefix` only if you know your project needs one value for
239
+ both.
240
+ - **`node:*-slim` images have no CA certificates.** `/etc/ssl/certs` is empty and
241
+ the `ca-certificates` package is not installed. Node carries its own roots, so
242
+ nothing in JS notices — but `sentry-cli` is a Rust binary that uses the system
243
+ store, and it fails with `SSL peer certificate … unable to get local issuer
244
+ certificate`. Add `ca-certificates` to the build stage.
245
+
246
+ **What we have not measured:** how a given Sentry project resolves the uploaded
247
+ artifacts end to end. `sentry-cli sourcemaps upload` matches on debug IDs it
248
+ injects into the emitted JS, and the URL prefix is the older release-artifact
249
+ fallback — which is why the prefix is set correctly per directory rather than
250
+ left to one default, and why this paragraph is here instead of a claim.
212
251
 
213
252
  ## Catching errors — what's automatic, what's manual
214
253