@voltro/cli 0.57.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.
- package/CHANGELOG.md +256 -0
- package/dist/apiBuild-B9OvhITV.js +2 -0
- package/dist/{apiBuild-DDr2aNFd.js → apiBuild-BU5cd-Fn.js} +52 -50
- package/dist/bin.js +1 -1
- package/dist/build-uRMZrT8z.js +1066 -0
- package/dist/{checkCommand-BISqx1OJ.js → checkCommand-C1CQc8lD.js} +1 -1
- package/dist/{checkCommand-DUtMWjcR.js → checkCommand-DC7k1_hP.js} +3 -3
- package/dist/{codegenCommand-DnVuDxwT.js → codegenCommand-kS3Twuc9.js} +7 -7
- package/dist/{codemodRunner-BlQPfjzA.js → codemodRunner-kNnh1I7_.js} +1227 -911
- package/dist/{commands-CRbxgxv0.js → commands-cu34ENUT.js} +21 -21
- package/dist/{dashboardCommand-Bf_-Ne3P.js → dashboardCommand-gG1y4PFG.js} +5 -5
- package/dist/{dataCommand-BoBJJ-Gb.js → dataCommand-hNOGKz4K.js} +6 -6
- package/dist/{dev-DOEJXicj.js → dev-Ce2gEziv.js} +2422 -2419
- package/dist/dev-Dff_pdik.js +3 -0
- package/dist/{doctorCommand-BrWu67JZ.js → doctorCommand-CGjefPmk.js} +505 -284
- package/dist/doctorCommand-DXnNpKQT.js +2 -0
- package/dist/{dormancyCommand-xn2y-pJm.js → dormancyCommand-DYgI0Kf-.js} +3 -3
- package/dist/{embeddingsCommand-Cn5MbRDM.js → embeddingsCommand-yJivQSrI.js} +2 -2
- package/dist/{envCommand-UJmJIbs9.js → envCommand-ORJUf8r_.js} +8 -8
- package/dist/{evolveCommand-Db30twUy.js → evolveCommand-8CuFBlbQ.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{infoCommand-BjVXpMlP.js → infoCommand-D02A4WI2.js} +1 -1
- package/dist/inspect-CZvsmp8t.js +2 -0
- package/dist/{inspect-DZnan87F.js → inspect-PcC5J-Dl.js} +6 -7
- package/dist/manifestBuild-AUh31bFU.js +2 -0
- package/dist/{manifestBuild-DnbFKF6w.js → manifestBuild-BwiS4iYm.js} +1 -1
- package/dist/{probeCommand-C5fuN6Z2.js → probeCommand-BUYzc0CT.js} +1 -1
- package/dist/{runtimeTrace-BQL_lfz6.js → runtimeTrace-DDuKJgVi.js} +1 -1
- package/dist/{sdkgen-CMUPrDjH.js → sdkgen-vq1fF9D_.js} +1 -1
- package/dist/serveCommand-1R52mo6F.js +2 -0
- package/dist/{serveCommand-B_w-Mpb_.js → serveCommand-Dr4KXmTS.js} +726 -725
- package/dist/serveEntry.js +1 -1
- package/dist/{start-s25GAIgn.js → start-C75w6ftA.js} +1 -1
- package/dist/{start-YaUehtDV.js → start-DNtIlBcn.js} +641 -609
- package/dist/startEntry.js +1 -1
- package/dist/{updateCommand-C9n_Z_oG.js → updateCommand-Bw3mCUDT.js} +1 -1
- package/dist/updateCommand-inInBIAh.js +2 -0
- package/dist/webDev-Bzb8ZFus.js +2 -0
- package/dist/{webDev-BgdkyjP6.js → webDev-CBN9KkyQ.js} +154 -146
- package/dist/{webhooksCommand-CYXTNvXq.js → webhooksCommand-BPJQOGTe.js} +1 -1
- package/package.json +73 -19
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/_manifest.json +1 -1
- package/templates/agent-docs/ai.md +223 -0
- package/templates/agent-docs/authentication.md +34 -0
- package/templates/agent-docs/cli.md +7 -6
- package/templates/agent-docs/data.md +30 -0
- package/templates/agent-docs/database/transactions.md +48 -0
- package/templates/agent-docs/observability.md +13 -2
- package/templates/agent-docs/plugins/auth-supabase.md +21 -0
- package/templates/agent-docs/plugins/sentry.md +131 -9
- package/templates/agent-docs/schema-driven-ui.md +29 -0
- package/templates/agent-docs/whats-new.md +26 -165
- 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/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +7 -7
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-auth/package.json +7 -7
- package/templates/apps/frontend-blank/package.json +6 -6
- package/templates/apps/frontend-cms/package.json +8 -8
- package/templates/apps/frontend-collab/package.json +9 -9
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +6 -6
- 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 -7
- package/templates/apps/frontend-saas/package.json +7 -7
- package/templates/apps/frontend-spa/package.json +6 -6
- package/templates/apps/frontend-ssr/package.json +6 -6
- package/templates/apps/frontend-ssr-api/package.json +7 -7
- package/templates/apps/frontend-static-blog/package.json +8 -8
- package/templates/apps/frontend-status/package.json +7 -7
- package/templates/apps/mobile-app/package.json +4 -4
- package/dist/apiBuild-B83Cb2Rv.js +0 -2
- package/dist/build-QKP6Bm0J.js +0 -872
- package/dist/dev-Bl9HqtV7.js +0 -3
- package/dist/doctorCommand-DrQv9SL3.js +0 -2
- package/dist/inspect-B0hL41s0.js +0 -2
- package/dist/manifestBuild-UXrnUcXP.js +0 -2
- package/dist/serveCommand-Dtb48ffg.js +0 -2
- package/dist/updateCommand-DsXEAHbd.js +0 -2
- package/dist/webDev-B6ZMX42w.js +0 -2
|
@@ -111,21 +111,143 @@ What it does:
|
|
|
111
111
|
> ```
|
|
112
112
|
> It's a no-op on the server (SSR) and without a DSN.
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
### Values the bundle cannot know
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
Everything in that `sentry:` block is a literal frozen into the bundle. That is
|
|
117
|
+
right for a DSN and a release, and wrong for `environment` as soon as one image
|
|
118
|
+
serves more than one environment — one build job and several environment-bound
|
|
119
|
+
deploy jobs is an ordinary pipeline, and a baked `environment` is then true for
|
|
120
|
+
at most one of them. Leaving it out does not help: Sentry defaults a missing
|
|
121
|
+
environment to `production`, so a wrong tag is what you get either way, and a
|
|
122
|
+
wrong tag is worse than none because somebody acts on it.
|
|
117
123
|
|
|
118
|
-
|
|
124
|
+
There is no runtime channel to read instead — public values are baked at build
|
|
125
|
+
time by construction — so the value has to be computed where it is known. The
|
|
126
|
+
entry is generated anyway, so it can call something:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// app.config.ts
|
|
130
|
+
sentry: { dsn: '…', optionsFrom: 'src/sentryOptions.ts' }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// src/sentryOptions.ts
|
|
135
|
+
export default () => ({
|
|
136
|
+
environment: location.hostname.startsWith('stage-') ? 'staging' : 'production',
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The module's default export is called by the generated entry before
|
|
141
|
+
`initSentryBrowser`, may be async, and its result is spread LAST — so it
|
|
142
|
+
overrides the literals above it, which is the whole reason to reach for it.
|
|
143
|
+
|
|
144
|
+
### Or from the public env
|
|
145
|
+
|
|
146
|
+
Without an explicit option, the browser half reads
|
|
147
|
+
`VOLTRO_PUBLIC_SENTRY_DSN`, `VOLTRO_PUBLIC_SENTRY_ENVIRONMENT` and
|
|
148
|
+
`VOLTRO_PUBLIC_SENTRY_RELEASE` — the same shape the api half already has, where
|
|
149
|
+
`sentryPlugin()` takes no arguments and resolves `SENTRY_*` from the environment.
|
|
150
|
+
Declare them in your `defineEnv` contract and an app is finished without touching
|
|
151
|
+
`app.config.ts`.
|
|
152
|
+
|
|
153
|
+
**A public value must exist when the bundle is BUILT.** `voltro build` freezes the
|
|
154
|
+
declared public subset into the bundle — that is what makes it readable in a
|
|
155
|
+
browser — so these belong in the build (a Docker build-arg, a CI build step), not
|
|
156
|
+
in deployment env. Setting one as a deployment variable deploys cleanly and does
|
|
157
|
+
nothing; `voltro start` warns when it finds one, naming whether the bundle
|
|
158
|
+
carries that key at all.
|
|
159
|
+
|
|
160
|
+
The two halves are independent: the api plugin alone gives you server traces + errors; add the web half for the browser hop. For the full frontend→backend waterfall, run **both** — and both are on by default, so an app with the plugin on the api and `@voltro/plugin-sentry` as a dependency of the web app gets the whole trace from one `SENTRY_DSN` plus one `VOLTRO_PUBLIC_SENTRY_DSN`. Give both halves the same `release`.
|
|
161
|
+
|
|
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
|
+
|
|
164
|
+
## Source maps (every bundle, browser and server)
|
|
165
|
+
|
|
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
|
+
|
|
168
|
+
```ts
|
|
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' },
|
|
173
|
+
}
|
|
174
|
+
```
|
|
119
175
|
|
|
120
176
|
```sh
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
177
|
+
# the build needs both; the deployment needs neither
|
|
178
|
+
SENTRY_AUTH_TOKEN=sntrys_… SENTRY_RELEASE=1.4.2 voltro build
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
That emits the maps, uploads them through `@sentry/cli`, and then **removes them from the output**. Three things in that sentence are the reason it lives in the build rather than in your deploy script:
|
|
182
|
+
|
|
183
|
+
- **The moment.** There is no seam in a Dockerfile between "the bundle exists" and "the image is built". There is one here.
|
|
184
|
+
- **The release.** Sentry matches an artifact to an event by release, and the event's comes from `SENTRY_RELEASE` via `sentryPlugin`. Upload under a different value and no frame resolves — silently, because an upload that matched nothing looks exactly like one that worked. A build with an upload configured and no release **refuses** rather than uploading under nothing.
|
|
185
|
+
- **The deletion.** A `.map` left in `dist` is your source, downloadable by anyone. "We delete it in the deploy step" is a promise a failing build breaks, so the removal is a `finally`: the maps go even when the upload fails, and a configured upload that did not happen fails the build.
|
|
186
|
+
|
|
187
|
+
### Turning it on selectively
|
|
188
|
+
|
|
189
|
+
`app.config.ts` is TypeScript, so the ordinary branch is the answer:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
sourcemaps: process.env.CI
|
|
193
|
+
? { mode: 'hidden', upload: { org: 'acme', project: 'web' } }
|
|
194
|
+
: 'hidden',
|
|
195
|
+
```
|
|
196
|
+
|
|
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.
|
|
198
|
+
|
|
199
|
+
`keep` defaults to "keep them only if nothing consumed them": `false` when `upload` is set, `true` when it is not. Set `keep: true` beside an upload when you want both.
|
|
200
|
+
|
|
201
|
+
### The auth token is not a config field
|
|
202
|
+
|
|
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.
|
|
204
|
+
|
|
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)
|
|
126
222
|
```
|
|
127
223
|
|
|
128
|
-
|
|
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.
|
|
129
251
|
|
|
130
252
|
## Catching errors — what's automatic, what's manual
|
|
131
253
|
|
|
@@ -843,6 +843,23 @@ chat.regenerate() // re-run the last user prompt
|
|
|
843
843
|
| `regenerate()` | Re-sends the last user prompt as a fresh turn; `undefined` if there is none. |
|
|
844
844
|
| `sending` | `true` while a `send` is in flight. |
|
|
845
845
|
|
|
846
|
+
**You do not need `@ai-sdk/react`.** `useAgentChat` covers the three things it is
|
|
847
|
+
usually kept for:
|
|
848
|
+
|
|
849
|
+
- **Tool calls** — each message carries `parts`, and a `tool` part has
|
|
850
|
+
`toolName`, `state`, `input` and `output`. Render from `parts` rather than
|
|
851
|
+
`content` and a tool call looks the same live and after a reload.
|
|
852
|
+
- **Persistence** — the thread is a DB-backed reactive query, not client state.
|
|
853
|
+
Nothing is lost when the tab closes.
|
|
854
|
+
- **Resumption** — for the same reason. Reopen the tab mid-turn and the
|
|
855
|
+
in-flight assistant message re-renders from the row that is still being
|
|
856
|
+
written; there is no transport to reconnect and no replay to arrange.
|
|
857
|
+
|
|
858
|
+
So an app can drop `@ai-sdk/react` and `DefaultChatTransport` entirely. What
|
|
859
|
+
`useAgentChat` deliberately does not do is manage a transport of its own — it
|
|
860
|
+
composes the agent's synthesized `messages` subscription and `send` action, which
|
|
861
|
+
is what makes the reload behaviour free rather than implemented.
|
|
862
|
+
|
|
846
863
|
`threadId` is required — mint one per chat (a `useRef`'d uuid) and keep it stable
|
|
847
864
|
across renders, since it is the subscription key for the whole thread. Render
|
|
848
865
|
from `message.parts` (text / reasoning / tool / source / file) rather than
|
|
@@ -1577,6 +1594,18 @@ changes, so define it with `useCallback` or at module scope unless you want that
|
|
|
1577
1594
|
And the bus is per api runtime — an app talking to several apis subscribes once
|
|
1578
1595
|
per api name.
|
|
1579
1596
|
|
|
1597
|
+
**You do not need this hook to REPORT rpc failures.** Every event this bus emits
|
|
1598
|
+
is also published on the client error bus, so a reporter — the Sentry browser
|
|
1599
|
+
integration, or anything wired with `subscribeClientErrors` — already sees it,
|
|
1600
|
+
under `source: 'rpc.mutation' | 'rpc.action' | 'rpc.subscription'` with the rpc
|
|
1601
|
+
tag and `traceId` in `context`. This hook is for cross-cutting *policy* (redirect
|
|
1602
|
+
on `Unauthenticated`, toast on a network failure), not for telemetry; bridging
|
|
1603
|
+
the two by hand now reports twice.
|
|
1604
|
+
|
|
1605
|
+
Everything is published, including a reconnect storm. If that is too loud for
|
|
1606
|
+
your reporter, gate it in the subscriber — which failures are worth an event is
|
|
1607
|
+
your policy, and the bus cannot hold one for every subscriber.
|
|
1608
|
+
|
|
1580
1609
|
This is the read side of failures that already happened on the wire. To push a
|
|
1581
1610
|
client-side error the server never saw, call `reportClientError(error, context)`
|
|
1582
1611
|
instead. Note also that a subscription failure *after* data arrived reaches only
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# What's new in 0.
|
|
1
|
+
# What's new in 0.59.0
|
|
2
2
|
|
|
3
3
|
Read this FIRST when a task touches an area you have not worked in recently.
|
|
4
4
|
It is the cheapest way to notice that the framework grew the thing you were
|
|
@@ -7,192 +7,53 @@ workaround for something that shipped two versions ago.
|
|
|
7
7
|
|
|
8
8
|
BREAKING entries name a codemod; run `voltro update` to apply it.
|
|
9
9
|
|
|
10
|
-
###
|
|
10
|
+
### ⚠ BREAKING
|
|
11
11
|
|
|
12
|
-
- **@voltro/
|
|
12
|
+
- **@voltro/cli** — **`web.sourcemaps` moved to the top level of `app.config.ts`, and now covers every bundle the build minifies.**
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
It reached exactly one of them before: the vite CLIENT build. `voltro build` minifies four — the browser bundle, the SSR bundle, the `voltro start` bundle, and for an api app the precompiled entry plus the serve bundle that `voltro serve` actually runs — and none of the other three had a `sourcemap` option or a config path that could ask for one. `keepNames` rescues the identifiers that are read as strings and nothing else, so a frame out of the artefact production runs read `at Qo (serveEntry.js:236:76405)`.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
useFormBinding('app', (v) => (v.repeats ? 'calendarRecurringEvent.create' : 'calendarEntries.create'), { defaults })
|
|
18
|
-
```
|
|
16
|
+
The docs closed that hole with a sentence instead of a field: *server stack traces are un-minified already*. The built file says otherwise, and the server half is where it costs most — a schedule, a workflow step, a subscriber or a REST route has no browser caller whose frame could stand in for it.
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
A second field (`api.sourcemaps`) would have made the same mistake twice. "Do I want resolvable frames from this app" has one answer; how many bundles the build happens to emit is not the caller's business.
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
Three details that come with it:
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
- **The old spelling is REFUSED, not ignored.** A moved option that is silently dropped goes green, stops emitting maps, and is found by someone reading an unresolvable frame months later. - **Each output directory uploads under the prefix that matches how its files are named** — `~/assets` for the browser bundle, `~/` for the bundles a node process loaded. One prefix across both is wrong for one of them. - **The upload and sweep run at every exit of the build**, not after the client bundle. Sweeping there would have deleted the client maps before the server ones existed, leaving those neither uploaded nor deleted.
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
- **@voltro/plugin-audit** — **`redact*: 'shape'` now buckets a string's length below a floor: `string(<16)` rather than `string(6)`.**
|
|
24
|
+
`minifiedBundlesCarryMaps.test.ts` asserts the property over the SET: every bundler options object in the CLI that turns minification on must also decide `sourcemap`. That is what was missing — not one option, three, because nothing was watching the class.
|
|
28
25
|
|
|
29
|
-
|
|
26
|
+
**`voltro update` carries you across this** — codemod `0.59.0/01_sourcemaps_moved_to_the_top_level`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.59.0).
|
|
27
|
+
- **@voltro/ai** — **Speech has a cost path, and a built-in provider name can be taken over.**
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
Three changes, one thread: the speech seam existed and nothing could reach it.
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
- **@voltro/cli** — **`voltro logs --url` and `voltro traces --url` can read a deployed app now — and when they cannot, they say so instead of blaming your filters.**
|
|
31
|
+
**`registerSpeechProvider(name, provider, { replaceBuiltin: true })`.** Refusing `mock` / `openai` / `gateway` / `anthropic` outright was wrong, and the case that shows it is one the refusal could not see: a provider name is not always an alias the app chooses. `openai` is the segment 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. It keeps its own dispatch instead, for every provider rather than the colliding one. What the refusal protected is kept by putting the intent in the call.
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
**Registered providers resolve BEFORE built-ins.** The dispatch decided from a hard-coded list of built-in names, so without this the flag would let you register `openai` and never route to it — declared, documented, wired at zero call sites.
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
**`MeteredUsage` — a tally in characters or seconds.** Every speech vendor meters one of those; none meters tokens. A ledger that only knew tokens could not hold a speech call at all, so speech spend was not un-priced, it was *absent*. The token shape is untouched (it is what the AI SDK returns); the new shape is separate rather than a unit tacked onto `AiUsage`, because writing a character count into a field called `inputTokens` puts a lie in a row someone reads later.
|
|
39
36
|
|
|
40
|
-
`
|
|
37
|
+
`_voltro_ai_usage` gains a `unit` column, and `costSource` gains `'unpriced'`: a non-token unit with no supplied price is written with the real count and a zero cost rather than a fabricated one, since 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.
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
BREAKING for a caller that had narrowed `CostBreakdown['costSource']` to two values or reads `estimateCostUsd`'s return positionally. No call site changes SHAPE — the token path takes the same arguments and returns the same numbers — but a narrowing is user-authored code, so this carries a `manual` codemod rather than `none`: nothing here can be transformed (whether the third member belongs in your union is your decision), and the reader who needs it is exactly the one a "touches no user-authored code" line would have sent away. It also names the query that does not fail but answers wrongly: summing `_voltro_ai_usage` counts ACROSS units now adds characters to tokens.
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
- **@voltro/cli** — **Two `voltro doctor` rules for defects that are completely decidable from the source — and were reported because nothing decided them.**
|
|
41
|
+
**`voltro update` carries you across this** — codemod `0.59.0/02_costsource_gained_unpriced`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.59.0).
|
|
46
42
|
|
|
47
|
-
|
|
43
|
+
### Added
|
|
48
44
|
|
|
49
|
-
|
|
45
|
+
- **@voltro/ai** — **`GatewayModelInfo.raw`** — the `/v1/models` entry exactly as it came over the wire, beside the normalised fields.
|
|
50
46
|
|
|
51
|
-
|
|
47
|
+
Normalising is the right default: the union of `tags`, `supported_parameters` and `modalities` is framework knowledge, and every app on the gateway was writing those forty lines differently. But normalising is lossy by construction, and the fields it drops are not decoration — a picker built on the vendor's own capability tags, a pricing display that shows a structure we have not normalised, `zdr` and `no_training` kept apart instead of folded into `dataPolicy`.
|
|
52
48
|
|
|
53
|
-
|
|
49
|
+
Without `raw`, such an app fetches the endpoint a second time and maintains a second parser of the same response. One field rather than three more normalised ones, because the alternative asks the same question again at every field the gateway adds.
|
|
54
50
|
|
|
55
|
-
|
|
51
|
+
Absent on the injected-provider path, for the same reason `capabilities` is: that seam never saw a response, and an empty object would turn "no wire data" into the claim that the wire carried nothing.
|
|
56
52
|
|
|
57
53
|
### Fixed
|
|
58
54
|
|
|
59
|
-
- **@voltro/
|
|
60
|
-
|
|
61
|
-
The cross-instance overlap guard asks whether an occurrence is already running anywhere in the cluster. Its only evidence was the run row's `firedAt` against a constant, because that is all a row carried — and a constant that must never cut off a live run has to be longer than the longest one. So a pod killed mid-run left a `running` row that read as a live occurrence until the window expired. Against a half-hourly cron that is twelve missed occurrences from one restart.
|
|
62
|
-
|
|
63
|
-
`_voltro_schedule_runs` now carries **`heartbeatAt`**, bumped while the handler is in flight, and liveness is measured rather than assumed: silent for three beats is dead. A row with no beat — one written by a process still on the previous version during a rolling deploy — is dated by `firedAt` and bounded by the schedule's own `maxRuntimeMs`, because the watchdog records anything past it as `failed`. Same field and the same three cases as `_voltro_replace_in_progress.heartbeatAt`; it is deliberately not a lease, and nothing takes ownership of the run.
|
|
64
|
-
|
|
65
|
-
**The log line now carries what it measured.** `schedule: overlap skip` with `scope: 'cluster'` described two states — a peer genuinely working, and a corpse holding the schedule shut — while the word "overlap" asserted the first, and telling them apart meant reading the scheduler's source. It reports `heldBy` and `lastSeenMs` now.
|
|
66
|
-
|
|
67
|
-
**Two more defects found in the same function.** The query took the 200 most recent `running` rows across EVERY schedule and matched the name in JavaScript, so past 200 concurrent rows a live run falls out of the page and the guard silently stops guarding; the name is in the predicate now. And a `finishRun` whose update matched nothing returned in silence — the row stays `running`, which is exactly the state that suppresses the schedule, so the one write whose absence stops a cron had no failure signal at all.
|
|
68
|
-
|
|
69
|
-
`scheduling.scheduleHeartbeatMs` (env `VOLTRO_SCHEDULE_HEARTBEAT_MS`, default 30 s) is the cadence. Both boot paths pass the resolved value; a run shorter than one interval writes no beat and costs nothing.
|
|
70
|
-
- **@voltro/cli** — **`voltro dev` crashed on boot for every API-only app.**
|
|
71
|
-
|
|
72
|
-
`tryRunWebDev` asks `loadConfig` for the app's WEB config and gets `null` whenever there isn't one — which is every `type:'api'` project, and any config that fails to import. That null is expected and handled: the function returns `{ ran: false }` and the caller boots the api alone.
|
|
73
|
-
|
|
74
|
-
The `health` block's resolution was inserted ABOVE that guard, so the first thing the function did was read a field off the null:
|
|
75
|
-
|
|
76
|
-
```
|
|
77
|
-
TypeError: Cannot read properties of null (reading 'health')
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Two things about how it stayed invisible are worth more than the one-line fix.
|
|
81
|
-
|
|
82
|
-
**`tsc` had no chance.** The read is written `(config as { health?: … }).health`, and a cast on a maybe-null value erases exactly the nullability the compiler would have refused. The guard has no type-level protection, so it needed a behavioural one — `webDevNoWebConfig.test.ts` asserts the CONTRACT (`{ ran: false, exitCode: 0 }`, no throw) rather than the line order, and fails with the production error when the guard is moved back.
|
|
83
|
-
|
|
84
|
-
**And every symptom pointed somewhere else.** Eight integration files went red together, and all of them reported a TIMEOUT — `both replicas must boot`, at 300 s — because what a fixture observes is a process that never starts listening. A crash at boot and a slow machine are the same observation from outside, and the second is the expensive diagnosis to start with.
|
|
85
|
-
- **@voltro/plugin-billing** — **A production boot with no `STRIPE_SECRET_KEY` silently selected the MOCK billing provider. It says so now.**
|
|
86
|
-
|
|
87
|
-
`resolveProvider` picks `stripe` when the key is present and `mock` when it is not. The zero-config default is right — add the plugin, see checkout flows, no account needed. What was wrong is how it failed later.
|
|
88
|
-
|
|
89
|
-
`STRIPE_SECRET_KEY` is a deployment variable, and one that silently stops being set is a routine event: a rotated secret, a typo in a values file, a CI variable nobody ever created. When that happens, an app that has been charging customers keeps answering every charge, subscription and invoice call SUCCESSFULLY, reaches nobody, and leaves nothing in the log to find afterwards.
|
|
90
|
-
|
|
91
|
-
A mock chosen by ABSENCE, under `NODE_ENV=production`, now warns — naming the consequence rather than the selection, and both ways out (set the key, or declare `provider: 'mock'` so the reader knows it was a decision). An explicitly declared mock stays silent: warning about a choice somebody made trains the reader to ignore the line that matters.
|
|
92
|
-
|
|
93
|
-
`NODE_ENV` decides only whether it is worth SAYING, never what is selected. Both environments resolve the same provider — an env-conditional behaviour is the shape a staging box sails past, and it is pinned as its own case.
|
|
94
|
-
- **@voltro/cli, @voltro/protocol** — **A plugin's `declaredEnv: [{ required: true }]` now ABORTS THE BOOT when the value does not resolve. Nothing checked it before.**
|
|
95
|
-
|
|
96
|
-
The field has existed for a long time and ten-odd plugins fill it in. Three consumers read it — the env manifest, `voltro env`, and the secret MINT (which only handles `generate`, a secret that is OURS to invent). None validated `required`. Its own doc said "declaration only — metadata, not a read path", while `PluginEnvVar.generate`'s said "in production a missing secret refuses the boot": true for the minted kind, false for the one that matters — a third-party credential nobody can invent.
|
|
97
|
-
|
|
98
|
-
Declared, documented, read by three consumers, enforced by none. Every surface read as wired.
|
|
99
|
-
|
|
100
|
-
Every required entry now resolves before any plugin activates, on BOTH api boot paths, and a failure names the PLUGIN — the variable name alone appears in no file the reader owns, so without the attribution the next step is a grep through `node_modules`.
|
|
101
|
-
|
|
102
|
-
**A `secret: true` variable resolves through the configured secrets backend**, not just `process.env`, because that is where its value lives when one exists. A backend that cannot answer therefore fails the boot too — reported as `unreadable` rather than as an absent variable, because those are different facts and one of them sends the reader to the wrong file. A non-secret variable never touches the backend, so a misconfigured vault cannot fail variables that never used it.
|
|
103
|
-
|
|
104
|
-
Merging, when two plugins declare the same variable: `required` takes the stricter (a plugin that can live without the value must not weaken one that cannot), `secret` takes `true` (over-classifying hides something that did not need hiding; under-classifying prints somebody's credential into a dashboard — it is the only direction with a failure mode on one side). An app that declares the variable in `app.config.ts` overrides the plugin entirely.
|
|
105
|
-
|
|
106
|
-
**Upgrade note.** One first-party declaration is affected: `plugin-notifications`' `VOLTRO_VAPID_PRIVATE_KEY`, declared `required: true` and reachable only when web push is enabled. `voltro dev` mints it, as before; a production `voltro serve` with web push and no key now refuses to start, which is what that declaration has always said.
|
|
107
|
-
- **@voltro/client** — **A consecutive-failure ceiling was counting a tab's LIFETIME, and the screen it stranded had nothing left to move it.**
|
|
108
|
-
|
|
109
|
-
`wireAuthRefresh` rebuilds the transport when a call comes back `Unauthenticated`, and `maxConsecutive` bounds it: after that many refreshes *with no successful call in between*, it stops, because at that point the credential is not stale — it is refused. "With no successful call in between" was carried by `AuthRefreshHandle.noteSuccess()`, a method the HOST had to call, and the only host never called it.
|
|
110
|
-
|
|
111
|
-
The consequence is not the loop the ceiling guards against. It is the opposite: three token rotations over an afternoon exhaust the budget, and from then on the tab never refreshes again. Every later expiry rejects every subscription the page opens, a rejected entry is terminal for its transport by design, so the screen waits on a spinner with nothing left to wait for.
|
|
112
|
-
|
|
113
|
-
`RpcErrorBus` has a SUCCESS channel now (`onSuccess` / `emitSuccess`), emitted by the same pipeline that emits the errors — a snapshot in the subscription cache, a settled mutation, a settled action. `wireAuthRefresh` subscribes to it itself, so no host has to remember anything; `noteSuccess()` stays for a host with a better signal of its own and is no longer what the policy depends on.
|
|
114
|
-
|
|
115
|
-
**`useSubscriptionHealth(apiName)` is new**, and it exists because the state it reports cannot be derived from any single hook. A refused subscription never retries, so there is no second error to react to, and `SubscriptionFailed` presents `data: undefined` — the value every reading layer derives `loading` from. A wrapper hook passing `{ data, loading }` through therefore turns a refusal into a permanent skeleton, and passing those two through is the natural shape to write.
|
|
116
|
-
|
|
117
|
-
const { healthy, failed } = useSubscriptionHealth('app') if (!healthy) return <Banner tags={failed.map((f) => f.tag)} />
|
|
118
|
-
|
|
119
|
-
Keyed by TAG, not counted — two failures of one call are one broken thing — and scoped to the api's runtime, reset when that runtime is rebuilt: carrying a failure across a transport swap would report a call as broken that has not been tried since.
|
|
120
|
-
- **@voltro/plugin-notifications, @voltro/cli** — **About one generated VAPID key in 256 was 31 bytes, and a 31-byte key can never send a push.**
|
|
121
|
-
|
|
122
|
-
`createECDH('prime256v1').getPrivateKey()` returns the private scalar the way OpenSSL stores a BIGNUM — minimal length, leading zero bytes stripped. A P-256 scalar is a fixed-width field element, so a draw whose high byte is zero comes back 31 bytes (measured: 82 in 20 000), and two zero bytes gives 30 (1 in 20 000). Both generators had it: the plugin's `generateVapidPrivateKey`, and the `p256` branch of `voltro dev`'s secret mint.
|
|
123
|
-
|
|
124
|
-
The consumers were already right, which is what made this survivable and also what hid it. `vapidPublicKeyFor` REFUSES a non-32-byte scalar — a wrong VAPID key must fail loudly rather than at the first delivery. But the refusal happens inside `sendWebPush`'s `try`, so it came back as `{ kind: 'failed', status: 0 }`: the same outcome as the push service being unreachable. Nothing named the key. And the minted value is written to `.env.local` and stays, so an affected project's push sending is broken permanently and looks like a network problem forever.
|
|
125
|
-
|
|
126
|
-
Both generators left-pad now, and both packages assert the width on their OWN generator — deliberately not one reading the other's source, because a cross-package guard does not run when the suite is filtered to a single package.
|
|
127
|
-
|
|
128
|
-
**Worth recording is how nearly it was written off.** It surfaced as a single test failing in a full run and passing alone, roughly 1.6 % of the time — the exact signature of a saturated machine. Isolation "cleared" it twice. What settled it was not another isolated run but generating 20 000 keys and counting the widths: 0.41 % at 31 bytes is not noise, it is 1/256, and that number names its own cause.
|
|
129
|
-
- **@voltro/runtime, @voltro/cli, @voltro/client, @voltro/kv, @voltro/plugin-broadcast** — **Seven reported points, all from one round. Every one of them is information the process already had, not reaching the person who needed it.**
|
|
130
|
-
|
|
131
|
-
**A `SqlError` reaching the client as a Defect carried no detail.** The log line said `Failed to execute statement` — no table, no column, no operation — while the trace exporter had written `db.query.text` for the same statement. The handler-failure logger has walked the driver cause for a long time; the DEFECT frame did not, so a failure that reaches the client without passing through that logger left an operator with nothing. It leads with the driver's own message now and points at `_voltro_traces`.
|
|
132
|
-
|
|
133
|
-
**A defect that repeats IDENTICALLY ends the resumable stream.** The reconnect loop is built for a transport drop, and a transport drop does not repeat itself verbatim; a broken statement does. So one broken query spent the whole `maxReconnects` budget, and the user watched a spinner and then read "connection lost" — describing the opposite of what happened. TWICE, not once: the first failure is genuinely ambiguous, the second identical one is not. Also documented: `reconnects` is the RUN TOTAL and `maxReconnects` bounds CONSECUTIVE reconnects without progress, so "attempt {reconnects} of {maxReconnects}" renders `5/2`.
|
|
134
|
-
|
|
135
|
-
**`voltro build` reported a config that failed to LOAD as "no web app found".** It used the wrapper that discards the import error, so an `app.config.ts` that IS an api and simply threw came out as a claim about the app's TYPE. The message now distinguishes the two, prints the error, and names both types.
|
|
136
|
-
|
|
137
|
-
**The `subscriber-effect-without-once` rule found one of four.** The helper name matched by EQUALITY, so `notifyAbsenceRequested` fell through while `notify` reported — a prefix now. And it read only the handler body, so a write one call level down inside the same file was invisible; the file is already parsed. What it still cannot see (a write in another module) is SAID: the report prints how many subscribers declare no `once:` against how many the rule found an effect in, because "1 file(s)" read as an inventory.
|
|
138
|
-
|
|
139
|
-
**`voltro dev` had no probe surface on a web app**, and `/internal/*` did not 404 — it fell into the page router. An SPA shell answered 200 with HTML, a loader redirect answered 303, an auth guard answered 303 to `/login`; a kubelet reads all three as PASS. One of those pages had a loader calling the api, so the WEB pod's readiness hung on the API's reachability once per probe interval. All three boot paths now answer through one matcher, before routing — and `health: { path, liveness, readiness }` in `app.config.ts` makes the paths and the answers the app's, for both app types. "Ready" is a dependency ping for an api and its opposite for a screen that must keep showing its last frame.
|
|
140
|
-
|
|
141
|
-
**Every hot reload ended as a crash.** ioredis emits `error` on an EventEmitter, and node turns an unhandled `error` event into an uncaught exception — so a client with no listener made every close with commands in flight a process crash. Functionally harmless (the supervisor restarts) and every reload read as a crash loop in Kubernetes. Both `@voltro/kv` and `@voltro/plugin-broadcast` register one on every client. The watcher also ignores a tool's scratch file (`vitest.config.ts.timestamp-…mjs`), which was restarting the app twice per test run.
|
|
142
|
-
|
|
143
|
-
**A row filter's resolution is now reported.** Between "refuses everything" (which an older version did, loudly) and "applies nothing" there was nothing to see: every read would widen and no log, test or `doctor` would say so. The framework says so the FIRST time a filter resolves on a live read path — a fact from the running system rather than an echo of the declaration. And a new advisory `doctor` rule reports a handler that hand-writes a predicate on a table the registered filter already covers: never a leak (two identical predicates ANDed are as tight as one), but an authorization rule maintained in places the framework already knew about.
|
|
144
|
-
- **@voltro/client** — **A mutation, workflow start or upload fired from a mount effect hit the client boot window and failed with the stub's message. Only `useAction` waited it out.**
|
|
145
|
-
|
|
146
|
-
Between the first client commit — children render against the loading stub so `hydrateRoot` can adopt the SSR DOM — and the moment the supervisor resolves the real rpc client, `handle.client` is a proxy that throws on any access. Measured in real chromium at ~15–30 ms, and wider in an app that mints a token in `authHeaders` before connecting. `useEffect(() => { mutate(...) }, [])` is the ordinary shape that lands inside it.
|
|
147
|
-
|
|
148
|
-
`useAction.run` was fixed for exactly this, with a unit test and a browser measurement. `useMutation.mutate`, `useWorkflow`'s five callbacks and `useUpload.upload` had the identical defect for as long, and the failure was worse than a delay: the stub's `not-yet-resolved api` DISPLACED whatever the real outcome would have been, so the message a developer read described our plumbing rather than their call.
|
|
149
|
-
|
|
150
|
-
All four seams go through one `awaitResolvedApi` now, and each reads the handle from a ref at CALL time. A `useCallback` dep list cannot solve this however correct it is — the callback a mount effect fires was built on the first render, before the re-render that would rebuild it.
|
|
151
|
-
|
|
152
|
-
`useMutation` waits BEFORE staging optimistic patches, not just before the network call: the stub carries a different `cache` instance, so patches staged against it would land in a cache no live subscription reads and then be reverted against the wrong one.
|
|
153
|
-
|
|
154
|
-
**The rule is asserted over the SET** (`bootWindowSeams.test.ts`), because a per-hook test can only demonstrate one member. A fifth imperative hook inherits it by being written rather than by being remembered — which is precisely what did not happen the first time.
|
|
155
|
-
- **@voltro/cli** — **The boot's `reactivity:` line reported what the dialect COULD run, not what was running — so the deployment with no cross-replica fan-out was the one told it had none missing.**
|
|
156
|
-
|
|
157
|
-
`wireBroadcastBus` derived it as `postgres || mysql || mariadb`. A dialect is capable of a native transport; that is not the same as one being started. With `CDC=0` — which any deployment whose database grants no REPLICATION privilege has to set — the change reader never runs, and the boot still announced
|
|
158
|
-
|
|
159
|
-
reactivity: cross-instance via native binlog CDC (mariadb)
|
|
160
|
-
|
|
161
|
-
The crooked sentence is the smaller half. Without a broadcast plugin this line is the ONLY thing said about cross-replica reactivity, and the branch it displaced is the WARNING that there is none. It is also the line our own guidance names as the check for whether the native-CDC amplification applies to you, and there it answered yes where the truth was no.
|
|
162
|
-
|
|
163
|
-
It reads `store.changeScope` now — `'fleet'` exactly when change capture is running, and the same value `remoteChangesVisible` takes two calls later, so the honest signal was already in scope. Derived rather than enumerated: a sixth dialect with a native transport joins by itself, and an app that turned capture off because every table is `.nonReactive()` is covered without anyone remembering that path exists.
|
|
164
|
-
|
|
165
|
-
The enumeration was also wrong in the other direction — **mssql (Change Tracking) was absent**, so a deployment running a real cross-instance transport was being warned it had none.
|
|
166
|
-
|
|
167
|
-
And the warning that fires in the CDC-off case names the cause instead of the dialect. The old fallback told a mariadb operator to "use ... mariadb (binlog CDC)", which reads as boilerplate to the one person who needed to act on it.
|
|
168
|
-
- **@voltro/runtime** — **A row-filtered table reached through `.with(...)` is narrowed now, where it used to be refused — and before that, served unfiltered.**
|
|
169
|
-
|
|
170
|
-
An eager load resolves BELOW the seam that AND-merges the filter onto a read's base table: the stores expand the eager tree themselves, the memory store by recursing through its own raw read and the SQL stores by folding the relation into one join or JSON aggregate. Measured when it was found: a filter restricting a relation target to the caller returned exactly the caller's row on a direct read and BOTH rows through `.with()` on the same data.
|
|
171
|
-
|
|
172
|
-
That shipped as a refusal, on the reasoning that the real fix meant applying the filter inside eager compilation across four dialect stores. **It is not that change.** Every eager branch already accepts a `where`, and both resolvers already honour it — the walker ANDs it into the relation's lookup descriptor, the JSON compiler emits it into the correlated subquery on all five dialects. So the middleware writes the filter where a caller could have written it, and every resolver applies it without knowing a row filter exists. One rewrite, six execution paths, no dialect emitter touched.
|
|
173
|
-
|
|
174
|
-
A caller's own `where` is kept and the filter goes UNDER it: a branch you narrowed stays narrower, and nothing a caller writes can widen the filter. Nested `.with(...)` is narrowed at every level.
|
|
175
|
-
|
|
176
|
-
**One case still refuses: a row-filtered `manyToMany` JUNCTION.** A branch `where` is a predicate on the relation's TARGET, so a filter narrowing the junction has nowhere to be expressed. Narrow and honest beats a rewrite that looks complete.
|
|
177
|
-
|
|
178
|
-
Verified on the walker (unit, in-memory store) AND on the JSON-aggregation path against a real postgres, because those are two separate implementations and the suites that cover them each mock the other's half.
|
|
179
|
-
- **@voltro/client** — **Two dev-time messages said something the code had not measured.**
|
|
180
|
-
|
|
181
|
-
**The store's equals-footgun warning blamed the selector.** It read *"the selector returns a NEW object each call"* — and the site cannot observe that: an identical state returns from the cache before the selector runs, so everything reaching the warning arrives with a CHANGED state. Shallow-equal-but-not- identical there has two producers, and the message asserted one of them:
|
|
182
|
-
|
|
183
|
-
- the selector BUILDS (`s => ({ a: s.a })`), or - the STORE wrote an equivalent new value — `set({ crumbs: [] })` on leaving a page and again on entering the next is two different empty arrays.
|
|
184
|
-
|
|
185
|
-
Both waste the render and both are fixed by `{ equals: shallow }`, so the advice was right while the diagnosis pointed at the wrong half — and a deployment reading the second case went looking at a selector that had been returning the same reference all along. The warning now MEASURES which one it is: it re-runs the selector on the previous state (dev only, at most once per store, on a selector required to be pure anyway) and says either "the selector builds" or "the store is writing an equal value".
|
|
186
|
-
|
|
187
|
-
**And a validation template rendered its own placeholder.** `interpolate` left an unfilled `{param}` in place, so a template whose parameter was missing put a literal `Mindestens {min}` into the interface. It reports the hole now and the caller falls through to the issue's own rendered sentence, which is complete by construction. The reported route is closed twice over: counting rules supply BOTH `count` (the name an i18n layer pluralises on) and `min` (the built-in template's), so a framework-derived issue cannot reach it; a hand-written annotation still can, and a raw placeholder is never the right answer.
|
|
188
|
-
- **@voltro/cli, @voltro/ai** — **`@voltro/ai`'s resumable-stream tables were declared, bounded, documented — and never added to the migration set.**
|
|
189
|
-
|
|
190
|
-
`dataStoreResumableStreamStore` writes to `_voltro_stream_events` and `_voltro_stream_state`. `frameworkTableAssembly.ts` imports six `@voltro/ai` tables and did not import these two, so `voltro db plan` answered *schema is up to date* while both were absent, and the first user's turn died inside the store rather than at boot.
|
|
191
|
-
|
|
192
|
-
They are assembled now for any app that DEPENDS on `@voltro/ai`, so all four schema-declaring paths plan them.
|
|
193
|
-
|
|
194
|
-
**The gate is the dependency, not a file convention, and that is the half worth knowing.** Every other flag in `FeatureMix` reads a filename — `*.agent.tsx`, `*.connection.ts` — and `dataStoreResumableStreamStore(ctx.store)` has none: it is an ordinary call, reachable from an action, a query, a workflow step. Gating on `agents` is the obvious guess and it is wrong, because an app can stream without declaring a single agent file. The manifest is the superset that CAN reach the store, it comes from the source tree, and every process in one deployment computes it identically — the constraint the declared set is under.
|
|
55
|
+
- **@voltro/cli** — **`@sentry/cli` 3.x loads.** The loader read one export spelling — `module.exports = SentryCli`, which is 2.x. 3.x ships `exports.SentryCli = SentryCli`, so `mod.default` is a namespace object rather than the class, and a build with a source-map upload configured stopped with *could not be loaded*.
|
|
195
56
|
|
|
196
|
-
|
|
57
|
+
The framework cannot pin its way out of this: `@sentry/cli` is FSL-1.1-MIT, so it is not ours to depend on and the app installs it — which means `pnpm add -D @sentry/cli`, the command we ourselves print, resolves the latest. Reading one spelling turned our own documented instruction into a failing build.
|
|
197
58
|
|
|
198
|
-
|
|
59
|
+
All four positions are accepted now (both spellings, each of which `import()` presents twice), and the two failures are told apart: a package that resolved but had no readable constructor no longer says *install it*. That reading is what cost a pipeline — locally `require()` resolves and the binary is there, so nothing about the install looks wrong.
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.97.0",
|
|
14
14
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/ai": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/ai": "0.59.0",
|
|
16
|
+
"@voltro/cli": "0.59.0",
|
|
17
|
+
"@voltro/database": "0.59.0",
|
|
18
|
+
"@voltro/env": "0.59.0",
|
|
19
|
+
"@voltro/protocol": "0.59.0",
|
|
20
|
+
"@voltro/runtime": "0.59.0",
|
|
21
21
|
"effect": "^3.22.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-auth": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
22
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.59.0",
|
|
17
|
+
"@voltro/database": "0.59.0",
|
|
18
|
+
"@voltro/env": "0.59.0",
|
|
19
|
+
"@voltro/plugin-auth": "0.59.0",
|
|
20
|
+
"@voltro/protocol": "0.59.0",
|
|
21
|
+
"@voltro/runtime": "0.59.0",
|
|
22
|
+
"@voltro/sql-postgres": "0.59.0",
|
|
23
23
|
"effect": "^3.22.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.59.0",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
28
|
"@vitest/coverage-v8": "^4.1.10",
|
|
29
29
|
"vitest": "^4.1.10"
|
|
@@ -16,16 +16,16 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@effect/platform": "^0.97.0",
|
|
18
18
|
"@effect/rpc": "^0.76.0",
|
|
19
|
-
"@voltro/cli": "0.
|
|
20
|
-
"@voltro/database": "0.
|
|
21
|
-
"@voltro/env": "0.
|
|
22
|
-
"@voltro/plugin-multitenancy": "0.
|
|
23
|
-
"@voltro/protocol": "0.
|
|
24
|
-
"@voltro/runtime": "0.
|
|
19
|
+
"@voltro/cli": "0.59.0",
|
|
20
|
+
"@voltro/database": "0.59.0",
|
|
21
|
+
"@voltro/env": "0.59.0",
|
|
22
|
+
"@voltro/plugin-multitenancy": "0.59.0",
|
|
23
|
+
"@voltro/protocol": "0.59.0",
|
|
24
|
+
"@voltro/runtime": "0.59.0",
|
|
25
25
|
"effect": "^3.22.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@voltro/testing": "0.
|
|
28
|
+
"@voltro/testing": "0.59.0",
|
|
29
29
|
"typescript": "^6.0.3",
|
|
30
30
|
"@vitest/coverage-v8": "^4.1.10",
|
|
31
31
|
"vitest": "^4.1.10"
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-deactivation": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.59.0",
|
|
17
|
+
"@voltro/database": "0.59.0",
|
|
18
|
+
"@voltro/env": "0.59.0",
|
|
19
|
+
"@voltro/plugin-deactivation": "0.59.0",
|
|
20
|
+
"@voltro/protocol": "0.59.0",
|
|
21
|
+
"@voltro/runtime": "0.59.0",
|
|
22
22
|
"effect": "^3.22.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.
|
|
25
|
+
"@voltro/testing": "0.59.0",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
27
|
"@vitest/coverage-v8": "^4.1.10",
|
|
28
28
|
"vitest": "^4.1.10"
|