@voltro/cli 0.19.0 → 0.20.1

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 (62) hide show
  1. package/CHANGELOG.md +224 -0
  2. package/dist/apiBuild-C9aHz4Yh.js +2 -0
  3. package/dist/{apiBuild-CY5pEwwq.js → apiBuild-qZBEu59d.js} +2 -2
  4. package/dist/bin.js +41 -9
  5. package/dist/{commands-BvvoQL0v.js → commands-CA7jwejs.js} +1930 -1770
  6. package/dist/dbCommand-DA_RFj5K.js +2 -0
  7. package/dist/{dbCommand-DJSxnjPt.js → dbCommand-DFw2UoC-.js} +24 -24
  8. package/dist/{dev-C_E1XxNF.js → dev-2AvdzDg2.js} +1 -1
  9. package/dist/{dev-B6rfgKfo.js → dev-OHLsAq4K.js} +1488 -1462
  10. package/dist/index.js +1 -1
  11. package/dist/{inspectMetrics-D1DmLeJs.js → inspectMetrics-DvPNXmGA.js} +208 -179
  12. package/dist/{serveCommand-vPF5ucXC.js → serveCommand-BKqTKWTX.js} +341 -341
  13. package/dist/serveEntry.js +2 -2
  14. package/dist/{start-Clvz4IJb.js → start-CXQ7WL1W.js} +376 -368
  15. package/dist/startEntry.js +2 -2
  16. package/package.json +17 -17
  17. package/templates/AGENTS.md +1 -1
  18. package/templates/agent-docs/_index.md +1 -1
  19. package/templates/agent-docs/cli.md +33 -0
  20. package/templates/agent-docs/database/migrations.md +35 -2
  21. package/templates/agent-docs/database/misc.md +16 -0
  22. package/templates/agent-docs/database/seedsdialects.md +25 -0
  23. package/templates/agent-docs/database/transactions.md +15 -0
  24. package/templates/agent-docs/routing.md +13 -0
  25. package/templates/agent-docs/whats-new.md +78 -94
  26. package/templates/apps/api-ai/package.json +7 -7
  27. package/templates/apps/api-auth/package.json +8 -8
  28. package/templates/apps/api-backend/package.json +7 -7
  29. package/templates/apps/api-backend-deactivation/package.json +7 -7
  30. package/templates/apps/api-backend-mail/package.json +8 -8
  31. package/templates/apps/api-backend-mariadb/package.json +9 -9
  32. package/templates/apps/api-backend-storage/package.json +8 -8
  33. package/templates/apps/api-data-advanced/package.json +8 -8
  34. package/templates/apps/api-durable/package.json +8 -8
  35. package/templates/apps/api-feature-flags/package.json +9 -9
  36. package/templates/apps/api-governance/package.json +8 -8
  37. package/templates/apps/api-kv/package.json +8 -8
  38. package/templates/apps/api-moderation/package.json +8 -8
  39. package/templates/apps/api-observability/package.json +8 -8
  40. package/templates/apps/api-ratelimit/package.json +8 -8
  41. package/templates/apps/api-rbac/package.json +8 -8
  42. package/templates/apps/api-rest/package.json +7 -7
  43. package/templates/apps/api-saas/package.json +11 -11
  44. package/templates/apps/api-search/package.json +8 -8
  45. package/templates/apps/api-versioning/package.json +8 -8
  46. package/templates/apps/api-webhooks/package.json +9 -9
  47. package/templates/apps/changelog/package.json +6 -6
  48. package/templates/apps/edge-functions/package.json +2 -2
  49. package/templates/apps/frontend-admin/package.json +8 -8
  50. package/templates/apps/frontend-app/package.json +8 -8
  51. package/templates/apps/frontend-blank/package.json +7 -7
  52. package/templates/apps/frontend-contact/package.json +7 -7
  53. package/templates/apps/frontend-dashboard/package.json +7 -7
  54. package/templates/apps/frontend-docs/package.json +7 -7
  55. package/templates/apps/frontend-i18n/package.json +6 -6
  56. package/templates/apps/frontend-landing/package.json +7 -7
  57. package/templates/apps/frontend-spa/package.json +7 -7
  58. package/templates/apps/frontend-ssr/package.json +7 -7
  59. package/templates/apps/frontend-ssr-api/package.json +8 -8
  60. package/templates/apps/frontend-static-blog/package.json +6 -6
  61. package/dist/apiBuild-9NXH53Sd.js +0 -2
  62. package/dist/dbCommand-C3R5LrBZ.js +0 -2
@@ -1,4 +1,4 @@
1
- # What's new in 0.19.0
1
+ # What's new in 0.20.1
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,151 +7,135 @@ 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
- ### ⚠ BREAKING
10
+ ### Changed
11
11
 
12
- - **@voltro/plugin-audit** — **The durable audit sink stops writing credentials to a log table by default.** `redactInput` defaults to `'all'` the payload becomes `{ __redacted: 'all' }`, which still proves a payload existed. `redactInput: 'none'` restores the previous behaviour, and a function gives field-level control.
12
+ - **@voltro/database, @voltro/runtime, @voltro/plugin-versioning, @voltro/plugin-presence, @voltro/voltro** — Five framework-table indexes were holding GENERIC names in a namespace that is shared with your tables. Index names are unique per SCHEMA on every supported dialect, so `_voltro_row_history.index('byTrace')` reserved `byTrace` for the whole database — and `byTrace` is the first thing anyone reaches for when indexing a `traceId`. A consumer added `traceId` to their own audit table, indexed it the obvious way, and collided with ours; the framework's own error message even suggested renaming the framework's index as the fix.
13
13
 
14
- `AuditEvent.input` is the raw mutation input, so an unredacted trail is where a password change, an API key at issuance and a PAT land the one place nobody thinks to look for a credential. Losing payload detail is visible the first time you read a row; leaking a credential is not visible at all, which is why the default moved rather than staying opt-in.
14
+ Renamed: `_voltro_row_history` `byTrace` `byRowHistoryTrace`, `bySubject` `byRowHistorySubject`; `_voltro_api_keys` `byTenant` `byApiKeyTenant`; `_voltro_presence` `byChannel` `byPresenceChannel`; `_voltro_connections` `bySubject` `byConnectionSubject`. These are `_voltro_*` tables, so the rename rides the declarative differ on `voltro db apply` / boot no codemod. Adopters see a one-time index rebuild.
15
15
 
16
- **It is deliberately NOT driven by `.serverOnly()` / `.sensitive()`,** which is the design a consumer proposed and the one that cannot work: those markers live on TABLE COLUMNS and this is a mutation's INPUT. `changePassword({ oldPassword, newPassword })` has no column to consult, so a marker-driven default would cover exactly 0% of the motivating case while reading, to whoever configured it, like protection. (`.sensitive()` is also the export axis, not "unsafe to log" the category error the three-marker table exists to prevent.)
16
+ A test now enforces the rule that most framework tables already followed: a framework index name must MENTION its own table. Mechanical, so it cannot rot the way a curated list of "generic" names would, and it does not demand the full `_voltro_<table>_<name>` form which would force renaming ~20 already-safe indexes for no benefit. It also asserts no two framework tables claim the same index name, since installing two such plugins together would fail at migrate time for a reason neither plugin's author could see.
17
17
 
18
- Also:
18
+ ### Fixed
19
19
 
20
- - **`record: 'all' | 'errors' | predicate`** filters by OUTCOME, where `include`/`exclude` filter by tag. `'errors'` is the forensic core and pairs with `plugin-versioning` for the successful writes. **`'all'` stays the default** on purpose: defaulting to errors would silently stop recording successes on upgrade, and "what did this compromised account touch" is answered by successes. - **`errorTag`** — the typed error's `_tag`, flattened out of the `outcome` json and indexable. `null` for an untagged failure rather than a guess: "this had no tag" and "the tag is 'Error'" are different, and a column that invents the second makes every filter on it quietly wrong. - **Two indices for the questions asked under pressure** `(subjectId, status, at)` and `(tenantId, status, at)`. "Every denied call by subject X in the last 30 days" and "every failure against tenant Y" were both unindexed; the existing `byAuditTag` / `byAuditTrace` cannot serve either. - **Retention registers itself** — 365 days, `VOLTRO_AUDIT_LOG_TTL_HOURS`, drained by the boot sweep. "Pair it with the governance sweep" was a docs sentence rather than a default, so nobody did. - **Erasure is deliberately NOT auto-registered.** Erasing a subject must not delete the record that they were refused four hundred times — that record *is* the evidence. Anonymise instead; the docs carry the `subjectScopes` entry to paste, and it stays a decision the app makes explicitly.
20
+ - **@voltro/sql-mysql, @voltro/voltro**A MariaDB table with a UNIQUE constraint on an UNBOUNDED text column can never be decoded from the binlog. The reader now says so ONCE with the real cause and a remedy that works — and excludes the table, instead of looping on it forever.
21
21
 
22
- **Migration** `voltro update` prints it (`0.19.0/02_audit-redact-input-default`, `manual`, and it fires only for apps that mount the plugin). Nothing stops compiling and existing rows are untouched; what changes is what the NEXT row records. Keep the new default unless you know your mutation inputs carry no secrets; pass a function for field-level control; or opt back in explicitly with `redactInput: 'none'`. The codemod deliberately does NOT write `'none'` into your config a transform could do it perfectly, which is exactly why it must not: it would pin every adopter to the behaviour the default moved away from and report the migration as complete.
22
+ **The mechanism.** MariaDB backs an unbounded UNIQUE with a **HASH long-unique index**, which adds a hidden `DB_ROW_HASH_n` column to the InnoDB row. That column IS in the binlog row image and is NOT in `information_schema.COLUMNS`, so the reader compares N+1 against N and throws on every write to that table:
23
23
 
24
- *Why this is `BREAKING` and not `Changed`: it is a silent behaviour change on upgrade. Nothing fails, which is the problem — an operator who never reads this section keeps a trail that has quietly lost its payload detail. `BREAKING` is what routes it into `voltro update`.*
25
- - **@voltro/cli, @voltro/database** **`.serverOnly()` now gates where it said it did.** A wire-reachable query that declares a `.serverOnly()` column of its source table in its `output` fails the boot under `voltro serve`, and makes `voltro doctor` exit non-zero. `voltro dev` still warns.
24
+ ```text
25
+ Table app.sessions schema changed between binlog event and metadata fetch:
26
+ the event has 9 columns, fetched metadata has 8
27
+ ```
26
28
 
27
- It shipped as one `log.warn` and nothing else — in every command — while `ColumnBuilder.serverOnly()`'s own doc comment and the seeded `AGENTS.md` marker table both said "**the boot audit — it FAILS the boot**, it does not warn". A team read the strong sentence, adopted the marker on four credential columns, injected a deliberate leak to check, and watched the server come up serving the leaking query. That is the register this repo keeps meeting from a new angle: *a check that prints instead of gating still reads as coverage* — here on the one marker whose entire job is the enforcement.
29
+ Nothing is broken; the table is shaped that way, permanently. The previous recovery (skip to the current binlog end) recovered nothing, because the end is exactly where the next failing write appears a loop a consumer measured at roughly every 9 seconds, re-signalling resync to the whole fleet each pass.
28
30
 
29
- Two smaller things went with it, both of which had misled the reporter:
31
+ **The cause we shipped in the previous entry was WRONG, and this retracts it.** It blamed a `DROP COLUMN` that ran as `ALGORITHM=INSTANT` leaving a phantom column, and told people to run `ALTER TABLE … FORCE`. The same consumer measured that: 9 InnoDB columns before the rebuild, 9 after, hidden column still present — the rebuild recreates the index and therefore recreates the hidden column. The repair line sent readers in a circle. They also disproved the version theory, being on the same MariaDB 11.8 we had tested on and failed to reproduce a phantom column with.
30
32
 
31
- - **The message names the command.** It was emitted through a module-level logger scoped `voltro:dev`, so a warning from `voltro serve` announced itself as dev — which is why they concluded, and reported, that the audit does not run in production at all. It did; it just misattributed itself and stopped nothing. - **The audit is computed once, in `loadDiscovered`**, the discovery dev / serve / doctor / check all share — the same reasoning `validateRegisteredRelations()` lives there for. Dev and serve may disagree about what a leak COSTS; they must not disagree about what a leak IS.
33
+ **Now:** affected tables are found at CDC start by a privilege-free probe the direct evidence in `INNODB_SYS_COLUMNS` needs `PROCESS`, which an app DB user does not have, so the constraint SHAPE is inferred from `information_schema.STATISTICS` + `COLUMNS` instead reported once as an error naming `text().maxLength(n)` as the remedy and `ALTER TABLE FORCE` as explicitly not one, and EXCLUDED from the reader.
32
34
 
33
- `VOLTRO_SERVER_ONLY` moves the line both ways: `strict` fails `voltro dev` too, `warn` downgrades serve, `off` silences it. The downgrades are documented rather than hidden, because the alternative to a stated escape hatch is deleting the marker, and a check whose only way out is to disable it gets disabled.
35
+ Excluding is what makes it converge, and that is measured rather than assumed: an excluded table with a hidden hash column produces no reader error at all, while the same table included throws on the first write. Cross-instance change events for such a table are lost until it is bounded; own-node reactivity is unaffected (writes still emit inline).
34
36
 
35
- `voltro check` deliberately does NOT run it: it has a live-api mode with no access to your table definitions, and a rule that fires in one of its two modes is worse than one that fires in neither.
37
+ Framework `_voltro_*` tables cannot hit this they are filtered out of the reader's include list before it reaches the replication client, and exclusion demonstrably shields the metadata fetch.
36
38
 
37
- **Migration** `voltro update` prints it (`0.19.0/01_server-only-gates-the-boot`, `manual`, and it fires only for apps that actually use the marker). Run `voltro doctor` BEFORE you deploy: it reports exactly what `voltro serve` will now refuse, with no deploy involved. Each finding has two honest fixes and only its author can choose — the column is not wire-safe (drop it from the query's `output`, keep the marker), or the marker is wrong (drop the `.serverOnly()`). Do not substitute `.encrypted()`: that is the at-rest axis, the runtime decrypts for the handler, and reading it as "safe to expose" is the category error the three-marker table exists to prevent. To ship while triaging, `VOLTRO_SERVER_ONLY=warn` downgrades serve back to a warning — a bridge, not a setting to keep.
39
+ **Caveat worth reading if you are already affected:** on a table that ALREADY exists, adding `.maxLength(n)` currently changes nothing the schema differ does not diff text length, so it plans 0 operations and reports "up to date". That is a separate defect, reported in the same round and not yet fixed; until it is, the remedy only applies to newly created tables.
40
+ - **@voltro/cli, @voltro/voltro** — `voltro codegen` no longer writes a silently plugin-less `rpcGroup.generated.ts`, and it now reports what it merged.
38
41
 
39
- *Why this is `BREAKING` and not `Changed`: no signature moves and nothing that compiled stops compiling, so the literal type-level test does not catch it. It can still turn a booting production app into one that refuses which is the point, the boot it refuses is the one shipping the columnand that failure lands at DEPLOY time. Filing it as `Changed` would have kept it out of the one section the stability contract names as the migration path, and out of `voltro update` entirely, because codemods hang off `BREAKING`. Both doc claims were corrected in the same change, so the `.d.ts`, the agent template and the docs site now describe the same behaviour.*
42
+ `loadApiConfig` swallows every failure into `null`, and `config?.plugins ?? []` turned that into "this app has no plugins". So an `app.config.ts` that threw while importing produced a generated file with **no plugin error union and no plugin routes**followed by `voltro codegen: wrote rpcGroup.generated.ts`. The file typechecks, so nothing downstream catches it; the only symptom is a client branching on an error tag that never arrives.
40
43
 
41
- ### Added
44
+ A consumer with ~140 declarative `guards:` measured that file 2781 lines shorter after a version bump, with the `ScopeError` import and the whole `__voltroPluginErrors` union gone. For the record, since they were careful to separate measurement from conclusion: the generator did NOT drop the feature — the plugin-codegen path is byte-identical between 0.19.0 and 0.20.0, and the published `@voltro/cli@0.20.0` does contain the identifier they grepped for. Their `grep` came back empty because the bundled chunk contained a literal NUL byte, which makes a file binary to most search tools (fixed separately, and it had been hiding files from our own audits too). What was real is the artefact diff, and this is the path that produces it without a word.
42
45
 
43
- - **@voltro/plugin-audit** **`auditByTrace` / `auditBySubject` — the read side of the correlation join.**
46
+ Now: a config that EXISTS but fails to load is a refusal with a non-zero exit and the underlying cause, not a quiet downgrade. An app with no `app.config.ts` at all still generates absence is legitimate, failure is not. And every run prints `(plugins N, error schemas N, plugin routes N)`, because a count that drops from 7 to 0 has to be visible in the success line or the next occurrence is found the same way: by diffing artefacts during a debugging session.
44
47
 
45
- `byAuditTrace` and `byAuditSubjectStatus` shipped in the same release with **no caller**. That is the identical defect the versioning side had and that its own entry points were added to fix, repeated on the other half of the join one file away: an index nobody can enter is a query the app still hand-writes, and the docs then demonstrate a raw select over a framework-internal table.
48
+ `loadApiConfigDiagnosed` is the new seam (`{ config, present, error }`); `loadApiConfig` is unchanged for every existing caller.
49
+ - **@voltro/cli, @voltro/voltro** — `ssr cold-compile` log lines now carry the compile's duration, and `voltro start` emits them at all.
46
50
 
47
- It surfaced by checking an adopting team's design document against the code rather than from memory. Their §6 asks for *"a read-side composition joining `_voltro_row_history` × the audit sink (on `traceId`) × `actors`"* — which needs BOTH halves to have an entry point, or neither is usable.
51
+ The lines had a `start` and an `end` and no timing, which looks readable and is not: cold compiles run concurrently up to `VOLTRO_DEV_SSR_COMPILE_CONCURRENCY`, so the pairs INTERLEAVE. Subtracting adjacent timestamps names the wrong module, and above a limit of two they cannot be paired by eye at all — which is what a user reading a pod log actually hit, with three `start` lines before their `end`s:
48
52
 
49
- ```ts
50
- const calls = await auditByTrace(ctx.store, traceId) // who called, and was it refused
51
- const changed = await historyByTrace(ctx.store, traceId, tenantId) // what it changed
52
- const denied = await auditBySubject(ctx.store, actorId, { status: 'error' })
53
+ ```
54
+ …:59.704 ssr cold-compile start id=…/layout.tsx
55
+ …:59.704 ssr cold-compile start id=…/(main)/layout.tsx
56
+ …:03.447 ssr cold-compile end 3743ms id=…/layout.tsx
53
57
  ```
54
58
 
55
- `auditBySubject` takes `status` as a real argument rather than leaving the caller to filter in JS the index is `(subjectId, status, at)`, so a filter applied after fetching would not use it. `limit` defaults to 100, because an actor's history is unbounded and an entry point that returns all of it is one you call once in production.
56
-
57
- *They were also not exported from the package index when first written — built, tested, and unreachable. Caught before shipping; worth recording because "it has a test" and "a consumer can call it" are different claims.*
58
- - **@voltro/database, @voltro/runtime, @voltro/protocol, @voltro/cli, @voltro/plugin-versioning, @voltro/plugin-audit, @voltro/voltro** — **`ChangeEvent` carries the calling `traceId` and `subjectId`** — the join key that lets `plugin-versioning` (what changed) and `auditPlugin` (who called, and whether they were refused) be read as one trail.
59
-
60
- Both halves shipped and neither could be joined to the other. A consumer put it exactly right: *"we are not asking you to build our audit feature. We are asking for the join key that lets anyone build one on the three pieces you have already shipped."*
61
-
62
- **The mechanism, and the part that was an empirical question rather than a design one.** Identity is known one layer up (`subject` in the middleware, `traceId` at the rpc boundary) and the event is created several layers down, inside each dialect store's private emit. Threading a context argument through every `DataStore` method to reach it would change the port every driver implements, for metadata that is ambient by nature — so it rides an `AsyncLocalStorage` (`@voltro/database`'s `writeAttribution.ts`), the same shape as the existing trace and replica-routing contexts.
63
-
64
- Whether that survives a SQL store was *not* obvious: the write goes through `ManagedRuntime.runPromise`, so the read happens inside an Effect fiber, and a scheduler draining fibers from a shared loop would run them under the async context of whoever created the drain. If that were true the attribution would be silently ABSENT — a join key that is simply never there, on a trail nobody checks until an incident. Verified against a real sqlite store, including the transactional path (which queues events and flushes them post-commit, *outside* the scope — which is why the stamp goes at event CREATION, not delivery).
65
-
66
- - **One call site, not one per method.** The scope is established at the rpc executor boundary, so it covers every write the handler makes — `ctx.store`, `EffectStore`, the crud helpers, a plugin interceptor's own writes. Wrapping the store middleware instead would have meant wrapping each mutating method and hoping the next one added remembers. - **`subjectId` is the same identity `audit()` stamps** (`actingUserId`, now exported so there is one source). Two answers to "who wrote this row" on one write would be worse than one; which API *key* was used is recoverable from the audit row sharing the `traceId`. - **Absent is a fact, not a gap** — no request behind the write (seed, startup hook, schedule, workflow step), or an event from another replica, where stamping the local ambient trace would attribute a remote write to a local call. The keys are omitted rather than set to `undefined`, so an unattributed event is byte-identical to one from before this existed. - **`dev.ts` and `serveCommand.ts` no longer hand-mirror the plugin fan-out.** They built that object literal separately in two files; a field added to one and not the other gives a plugin the data in dev and silence in production, and both files typecheck alone. Now `toPluginChangeEvent`.
67
-
68
- `_voltro_row_history` gains `traceId` / `subjectId` plus `byTrace` and `bySubject` indices — "what did this call touch" and "what did this actor touch" were previously unanswerable at any speed, since `byRow` requires already knowing which row you are asking about. `changedBy` now prefers the caller over the row's `audit()` stamp, which fixes a reported case for free: the stamp is `null` for every write through the boot store, so a login route produced version rows with no actor at all.
69
-
70
- *`apiSurface: compatible`: every golden line this touches is either a pure addition (the new fields and `actingUserId`) or api-extractor renumbering an import alias — `Subject_2` became `Subject` across `runtime` and the `voltro` re-export because a new import changed the ordering. No declaration changed shape and no call site is affected.*
71
- - **@voltro/cli** — **`voltro doctor`'s hand-roll detector gains six rules and ranks its findings by file count.**
72
-
73
- An adopting team audited four apps by hand against the framework's "reach for instead" table and produced twenty items, each with a count (`1631` files with a manual `rows[0]`, `500` queries with `input.offset`, `306` forms, `368` relations declared against `17` uses). **The detector already reported eleven of those twenty, with counts.** These are rules for what it missed — the findings that made a person do work a scan should have done:
74
-
75
- - **`offset-pagination`** — `.offset(` / `input.offset` / `.skip(` on a list query. `hand-cursor` did not catch this: it wants `hasMore` AND `limit+1`, which is a hand-rolled *keyset* pager. Plain OFFSET is a different smell with a worse ending — O(n) in the page number, so it does not fail, it just stops loading once the table is big, on the tables (audit logs, time entries) that get big first. - **`oauth-token-table`** — an `accessToken`/`refreshToken` column on an app table → `defineConnection({ kind: 'oauth2' })`. The reporting team's version of this leaked: a `json()` column holding a Slack payload with the token inside, readable by every colleague in the tenant. - **`non-incremental-aggregate`** — a `count/sum/avg/min/max` aggregate with no `incremental:`, rescanned in full on every refresh. - **`external-state-library`** — jotai / zustand / mobx / redux → `defineStore`. A second runtime beside the reactive engine, and one that does not participate in the subscription graph. - **`hand-route-module`** — importing a hand-maintained `routes` / `urls` module instead of `.framework/routes.generated`, where a renamed page is a compile error rather than a 404 someone finds in production. - **`hand-permission-check`** — a component reading a permission bag by hand instead of `useCan` / `useResourceCan`.
76
-
77
- **And the findings are now ordered by file count rather than by the order the rules happen to be declared in.** A reader facing twenty findings acts on the top of the list, so an arbitrary order silently decides what gets fixed. The reporting team ranked their own work by exactly that number and had to count it by hand, because this printed the same facts unranked. Effort we cannot know; magnitude we can.
59
+ The gate had the number for free and threw it away. It is measured INSIDE the concurrency permit, so it is the module's own compile cost rather than the time it spent queued behind the limit those are different numbers and only one of them is a property of the module. A slow first paint is usually one slow module, and this is the line that names it.
78
60
 
79
- *Two of these rules were caught being wrong by their own tests before shipping: `offset-pagination`'s first version checked only `input.offset` and did not match its own fixture (a narrow rule reports nothing and reads like a clean codebase), and the ranking test passed with the sort deleted until the fixture was rebuilt so declaration order and count order disagree.*
80
- - **@voltro/plugin-versioning** — **`historyByTrace` / `historyBySubject` — the entry points the new indices existed for.** Plus their Effect twins.
61
+ A failed compile now says `FAILED` instead of `end`. Without that, a 3.7-second line for a module that threw read exactly like a slow but successful compile.
81
62
 
82
- The correlation bridge added `byTrace` and `bySubject` to `_voltro_row_history` in the same release, and shipped them with no caller: `rowHistory` requires a `rowId` you already have, which is the wrong way round during an incident, when what you have is a trace or an actor. The docs demonstrated a raw `ctx.store.select('_voltro_row_history')`, which is the shape an index is supposed to save you from writing.
63
+ `voltro start`'s middleware fallback constructed the same gate with NO callbacks, so an on-demand compile there produced no line whatsoever; it is wired now.
64
+ - **@voltro/cli, @voltro/voltro** — `voltro db plans`, `db drift` and `db restore-snapshot` worked on postgres only. On mysql/mariadb (and mssql and sqlite) all three died with:
83
65
 
84
- Both are tenant-scoped exactly like `rowHistory` (own-tenant rows plus null-tenant rows from untenanted source tables; `undefined` skips the filter, for system paths only). `historyBySubject` takes a `limit`, default 100, because an actor's history is unbounded and an entry point that returns all of it is one you call once in production and never again.
66
+ ```text
67
+ fatal unhandled cli error (FiberFailure) SqlError: Failed to execute statement
68
+ ```
85
69
 
86
- **And a bug this surfaced.** The stored-row `VersionRow` decoder was written inline inside `versionsOf`, before the bridge existed, and silently dropped `traceId` / `subjectId` so `rowHistory()` returned rows missing the very field the feature exists to carry. Extracted to one `rowToVersion` now shared by all three readers, which is why the drift was possible in the first place.
87
- - **@voltro/cli, @voltro/database** — **`setRowFilter` is now named in the always-loaded agent core**, with the distinction that makes it findable.
70
+ The cause is three `${sql('col')}::text AS ${sql('col')}` casts POSTGRES syntax, in read paths whose helper is still called `buildPgLayer`. `db apply`, which WRITES the same ledger table, has no cast and worked, which is exactly the split a consumer reported: the commands that read were broken, the one that writes was fine.
88
71
 
89
- A team migrating eleven hand-written row filters reported missing it **twice** — once in a full framework audit, and once while telling a colleague in writing that the framework has no row-filter primitive. The depth doc covers it well; a depth doc is opened by someone who already suspects the topic. The core's SERVER rubric asked *"a permission check?"* and answered with two BOOLEAN questions, so a reader holding a row-VISIBILITY question took the nearest-fitting answer and wrote the filter by hand. Eleven times.
72
+ The casts existed to stop a driver handing back a `jsonb` object or a `Date`. Normalising in JS gets the same result and cannot be dialect-specific, since drivers differ in whether a json column arrives parsed and whether a timestamp arrives as a `Date`.
90
73
 
91
- The rubric now asks it directly, and the pairing is the point:
74
+ Worth naming what it cost: `db drift` is the command whose whole job is "alert if live diverged from declared", and the consumer who found this had live divergence at the time. The specific detector and the general one were blind together.
92
75
 
93
- > `guards:` *may I call this procedure?* a typed `ScopeError`. > `setRowFilter` *which rows may I see?* → the rows are simply absent.
76
+ **And the error now names the failing statement.** Their verdict was the actionable part of the report:
94
77
 
95
- Plus the failure it deletes a `WHERE ownerId = me` in the list handler covers the query and **not** the subscription and the trap they lost time to: `load` must read through an UNFILTERED store, because applying the filter to its own loader recurses until the stack blows.
78
+ > *"the error names no statement the statement text (or even the operation name) > would turn this from a dead end into a bug report. We would have sent you the > failing SQL if the error had contained it."*
96
79
 
97
- Also:
80
+ Right twice over — they could not diagnose it, and neither could we from the report; it took reading our own source. `@effect/sql`'s `SqlError` carries the driver error in `cause`, and every supported driver puts the useful part there (mysql2: `code`, `errno`, `sqlState`, `sqlMessage`, usually `sql`; pg: `code`, `detail`, `hint`, `position`). The CLI's fatal reporter printed only the wrapper. It now walks the cause chain and prints the driver message, the codes and the statement — collapsed to one line, and saying `<not attached by the driver>` when there genuinely is none, because that is information too.
98
81
 
99
- - **`voltro doctor` gains `unexercised-row-filter`.** Their 7466 tests stayed green when the filter landed because not one passed `rowFilter:` to `makeTestContext`; they only noticed because they sabotage their own predicates before believing a pass, and four sabotages ran green. `makeTestContext({ rowFilter })` being opt-in is right — a process-global in a parallel suite would be worse — but the consequence is a visibility rule nothing verifies. Same class as a suite reporting `passed` with no database. - **`MATCHES_NO_ROWS` is exported from `@voltro/database`.** They wrote `eq('id', '')` for "this caller sees nothing", because the natural spelling `inSet(col, [])` is the one that is dangerous in most query builders: an empty `IN ()` gets dropped, and a dropped predicate does not narrow, it WIDENS to the whole tenant. Here it is safe — the SQL compiler emits `FALSE`, the memory evaluator returns false — but that held by accident, with no test enforcing it. Now pinned in both evaluators, including the mirror case: an empty `notIn` matches EVERYTHING.
82
+ Shape-based rather than `instanceof SqlError`, deliberately: the CLI catches errors that have crossed the serve/start bundle boundary, where two copies of `@effect/sql` make `instanceof` silently false — the failure mode this repo has already paid for elsewhere.
83
+ - **@voltro/cli, @voltro/voltro** — `voltro doctor`'s `plaintext-secret` rule no longer flags metadata ABOUT a credential. An audit row denormalising the public facts of an api key — `apiKeyId`, `apiKeyKeyId`, `apiKeyType`, `apiKeyOwnerId`, `apiKeyName` — had three columns already excluded by the `*Id` suffix, while `apiKeyType` and `apiKeyName` fired. Telling a team to encrypt the LABEL of a credential is how a rule earns being ignored.
100
84
 
101
- *The doctor rule was itself wrong when first written: it read the normal source set, which excludes test files, so it could never observe the passing case and would have fired unconditionally on every app. Caught by writing the negative test.*
102
- - **@voltro/database, @voltro/plugin-versioning, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-mssql, @voltro/sql-sqlite** **`versioningPlugin({ timing: 'in-transaction' })` the history row commits or rolls back WITH the change it records.** Default stays `'post-commit'`.
85
+ The exclusion now covers final words that cannot BE the credential `Name`, `Type`, `Kind`, `Label`, `Prefix`, `Suffix`, `Status`, `State`, `Scope(s)`, `Version`, `Count`, `Provider`, `Format`, `Note`/`Description`/`Comment`, plus the existing `Id` and the hash family. Deliberately NOT on the list: `Value`, `Secret`, `Token`, `Key`, `Password` — the words that name the thing itself. A false negative from an over-wide list is silent, so that is the failure mode the list is built against, and a test pins the words that must still fire.
86
+ - **@voltro/database, @voltro/cli, @voltro/voltro** `voltro doctor` no longer contradicts itself about the `.serverOnly()` wire audit. The same command on the same tree reported:
103
87
 
104
- Post-commit recording is lossy by construction: between COMMIT and the forked history write there is a window, and a process that dies inside it leaves the change permanent and the trail silent. The size of the window is not the point — the direction is. A missing entry cannot be told apart from "nothing happened", so a trail that can lose entries proves nothing. Retry does not close it either; the process that would retry is the one that died.
88
+ ```
89
+ human: serverOnly: NOT CHECKED | json: {'checked': True, 'leaks': 0}
90
+ ```
105
91
 
106
- This was the sole reason an adopting team could not replace their hand-rolled audit writercalled from **315 of 358 mutation handlers**, with the 43 misses being what happens to any rule that depends on someone remembering.
92
+ Two causes, both fixed. `registerRelations` refused a re-registration of the IDENTICAL relation object, so a process that executes a module twice looked like two conflicting declarations it now mirrors `registerTable`'s `existing === table` tolerance (a DIFFERENT block claiming the same name still throws). And doctor loaded the app three times per run; it now loads once, so every report sees the same outcome instead of the first one succeeding and the next failing.
107
93
 
108
- **What a recorder receives is an `append`, not a store.** Bound to the caller's transaction, insert-only. It cannot open a nested transaction (which throws by design), cannot read-modify-write its way into a deadlock, and "append-only" stops being a docs claim and becomes the shape of the only thing it is handed.
94
+ The consequence was worse than the noise: the throw aborted the wire audit, so the check that a token cannot reach a client had not run since the reporting app adopted the marker — and a CI gate written exactly as we documented (`fail on serverOnly.checked === false`) reported green on an app where the audit provably had not run. That is the "reads as coverage without being coverage" failure the `serverOnly` field was added to remove, reappearing in the field added to prevent it.
95
+ - **@voltro/sql-mysql, @voltro/voltro** — `insertIgnore` on MariaDB no longer reports a cause it cannot know, and no longer turns a REJECTED write into a silent "conflict". `INSERT IGNORE` downgrades EVERY error to a warning — foreign key, NOT NULL, CHECK, truncation — so the post-check's premise ("the insert was skipped ⇒ a unique constraint fired") does not hold on this dialect. It asserted a second unique index that did not exist; the real cause was an FK (an auto-stamped `createdBy` with no matching `actors` row), and a consumer spent the diagnosis looking for a phantom index.
109
96
 
110
- **The rejection is the guarantee, not a defect.** When the history insert fails, a transaction offers exactly two outcomes: the mutation fails with it, or the error is swallowed and the change commits without its entry which is post-commit's hole with the cost already paid. There is no third option, so recorders do not swallow.
97
+ The message now reads the real error from `SHOW WARNINGS` on the same connection — BEFORE the existing-row lookup, since that lookup is itself a statement and resets the warning list. A non-duplicate warning is reported as a rejection and throws, because returning there is data loss presented as a normal outcome: the row is not written and the caller is told it already was. A genuine duplicate on an unnamed constraint now names the constraint that fired. Outside a transaction the warning cannot be attributed to our own statement (each statement acquires from the pool independently), so the message says the constraint is unknown rather than guessing — framework mutations are auto-transactional, so the common path has the cause.
98
+ - **@voltro/logger, @voltro/cli, @voltro/database, @voltro/voltro** — `voltro doctor --json` and `voltro capabilities --json` now emit exactly one JSON document on stdout. A `log.warn` from module discovery landed there ahead of it, so:
111
99
 
112
- **Why the default did not move.** In-transaction makes the history table a hard dependency of every covered write path: its availability becomes your write path's availability, and every covered write holds locks longer. Post-commit loses at worst one entry; in-transaction can at worst stop writes to the covered tables. Right trade for a compliance trail, wrong one for the undo / time-travel use this plugin also serves.
100
+ ```console
101
+ $ voltro doctor --json 2>/dev/null | python3 -c 'import json,sys; json.load(sys.stdin)'
102
+ JSONDecodeError: Extra data: line 2 column 1
103
+ ```
113
104
 
114
- **It refuses the in-memory store at boot** rather than silently no-op'ing. `memory` is the default dev store; an option that appears to work where it is cheapest to try and stops where it matters is worse than one that says so. Two limits hold in both timings and are documented: `store.raw()` produces no change event and is absent from the trail, and a write made outside a transaction is recorded immediately after rather than atomically.
105
+ Note the `2>/dev/null` in that repro stderr was already redirected, so there was no shell-side workaround. And it only happened when a warning fired, so a consumer's CI parsed the document correctly until one file out of 368 tripped one. That is the same failure the `serverOnly.checked` field was added to remove an automat unable to separate the normal case from the special case one layer out, in the surface added to fix it.
115
106
 
116
- *Two design claims in the plan for this were wrong and are corrected there. "Twelve write sites across three layers, no funnel" was true of `storeMiddleware` and irrelevant each dialect store funnels every write through one private `routeEvent`, which is where this hooks. "Bulk writes have no per-row post-image" was simply false: `updateMany` already issues `RETURNING *` and emits one event per affected row, so bulk needed no special case at all. Both were found by looking in the middleware instead of the store twice.*
107
+ A command that owns stdout for machine output now calls `claimStdoutForJson()` before doing any work that could log, and every record goes to stderr from then on. The stream decision itself moved into ONE place (`@voltro/logger`'s `stream.ts`, exported as `routeDiagnosticsToStderr`): the Effect surface and the direct surface each carried their own copy of `level === 'error' ? stderr : stdout`, and two copies of one rule is how the rule failed to change.
117
108
 
118
- ### Fixed
109
+ **Also fixed, same report:** the warning that started it was itself wrong. A `*.relations.ts` whose `relations(...)` map is EMPTY was reported as *"no relations(...) export found"* — pointing the reader at a missing export that is right there. `isRelationsSpec` rejects an empty map (correctly — there is nothing to register), but the caller could not tell that apart from a module with no export at all. It now says the map is empty and names the export.
110
+ - **@voltro/cli, @voltro/voltro** — The SSR bundle build now externalises a bare specifier it cannot resolve instead of aborting, so an uninstalled OPTIONAL peer no longer makes `voltro build` impossible.
119
111
 
120
- - **@voltro/cli** **A `voltro serve` that refuses to boot says why, instead of blaming the serve bundle.** A deliberate refusala missing `VOLTRO_SESSION_SECRET`, a `.serverOnly()` leak came out of the launcher as:
112
+ The SSR step runs with `ssr: { noExternal: true }` inlining everything is what lets a production web image ship without a framework dependency tree and that left no escape for a package that cannot be resolved at all. The commonest such package is an optional native peer reached through a library's Node entry:
121
113
 
122
114
  ```
123
- [voltro] serve bundle failed to load: VOLTRO_SESSION_SECRET is not set — refusing to serve. …
124
- [voltro] FATAL: production `voltro serve` requires a precompiled serve bundle at …
125
- but it is missing or failed to load. Run `voltro build` before serving …
115
+ Rolldown failed to resolve import "canvas"
116
+ from ".../konva/lib/index-node.js"
126
117
  ```
127
118
 
128
- The real reason is on the first line, under a wrong headline, followed by a louder and more confident wrong instruction. An operator whose first deploy forgot the session secret is told to rebuild an artefact that is fine — and rebuilding it produces the identical output, so the loop has no exit.
119
+ `konva`'s `main` is its Node build, which requires the optional native `canvas`; its `browser` field points at one that does not. An app that never renders to a canvas server-side has nothing to install.
129
120
 
130
- The cause is a catch that has to exist: `bin/voltro.mjs` imports the precompiled serve bundle inside a `try`, because an unusable bundle must degrade to the tsx path rather than kill the boot. It could not tell "this artefact is broken" from "this app decided not to start". Refusals now carry a marker (`bootRefusal.ts`) and the launcher prints them and exits 1.
121
+ A consumer measured that there was no way out from their side either, and each measurement is worth keeping: the import was ALREADY dynamic (rolldown must still resolve it to form the chunk), `renderMode: 'spa'` does not help (`.framework/app.tsx` imports every page statically for the router, so the module is in the SSR graph whatever the render mode), and an `ssr.external` passthrough in `app.config.ts` is not read. So `voltro build` — and with it the production image was unavailable for that app.
131
122
 
132
- The marker is the error's `name`, a plain string, rather than a class: the serve bundle INLINES the framework, so the thrown Error crosses an instance boundary where `instanceof` does not survive the same reason the core-table registry is keyed by `Symbol.for`.
123
+ The api serve bundle and the web start bundle already did exactly this; that plugin is esbuild's and this step is vite/rolldown, so it is the same probe behind a different interface. Framework packages (`@voltro/*`, `@effect/*`, `effect`) are never externalised, so the "needs nothing from node_modules" property still holds.
133
124
 
134
- Found by running the new `.serverOnly()` gate against a real fixture rather than by reading it, which is also how the misleading pair came into view: the session-secret case had been shipping that way for a while.
135
- - **@voltro/sql-postgres, @voltro/sql-mysql** — **`insertIgnore` explains a second-unique conflict instead of reporting an internal invariant.** The message was `row conflicted but lookup found nothing`, which tells a caller nothing they can act on.
125
+ Every externalised specifier is NAMED in the `SSR bundle ready` line. Externalising is right for an uninstalled optional peer and wrong for a genuine missing dependency — it trades a loud build failure for a quiet runtime one — and only the reader can tell which, so it is reported rather than swallowed.
126
+ - **@voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql, @voltro/sql-postgres, @voltro/database, @voltro/voltro** — A typed error thrown inside a mutation now reaches the client TYPED, on every dialect. It arrived as an untagged `Die` defect on mysql/mariadb, sqlite and mssql: `transactional()` settled its program with `runPromise`, which rejects with Effect's `FiberFailure` wrapper, and the wrapper copies `message` and a decorated `name` but nothing else no `_tag`, no payload, no prototype. So the rpc encoder could not match the failure against the mutation descriptor's `error:` union:
136
127
 
137
- It is reachable by ordinary means, and on mariadb it is the *common* path: `INSERT IGNORE` swallows ANY unique violation, so a row with a fresh `id` and a duplicate `slug` is skipped, and the lookup by the named `conflictColumns` then finds nothing. A team with `tenants (id PK, slug UNIQUE)` hits it on the first duplicate slug.
138
-
139
- The message now names the columns that were checked, the table, and the actual cause — a different unique constraint fired, and `insertIgnore` models one conflict target. This is step 1 of `plans/framework-insertignore-any-unique.md` and is deliberately independent of the feature: whether or not `conflictColumns: 'any'` ever ships, this error should have been readable.
140
- - **@voltro/cli** — **`ui/unlinked` and `ui/orphaned` resolve through barrel re-exports.** A `*.component.ui.tsx` reached only via `export { X } from './x'` was reported as unrendered, however many pages actually rendered it.
141
-
142
- On the app that reported it, `PageContent` is imported by 42 pages — every one of them through `@/components/shared` — and doctor said `imported only by: index.ts, its own test`. It was the last false positive standing after the alias fix took that app from 16 findings to 2.
128
+ ```
129
+ └─ ["error"] └─ ["_tag"] └─ is missing
130
+ Expected never, actual (FiberFailure) NotFoundError:
131
+ ```
143
132
 
144
- The rules ask "does anything RENDER this". A barrel is a real importer and renders nothing, so stopping at the first importer answers a different question than the one asked but only in an app that has an `index.ts`, which is why it survived.
133
+ Framework mutations are auto-transactional, so this was EVERY typed mutation error in an app. Nothing failed `defineMutation({ error: })` compiled, the client's type still said `NotFoundError`, and the `error._tag === 'NotFoundError'` branch was simply never taken at runtime. Actions, which are not auto-transactional, marshalled correctly the whole time, which is what made the transaction the discriminator. A hand-rolled error class lost its fields and its `instanceof` too; only `message` survived, which is why a workaround built on `error.message` looked like it worked and hid this.
145
134
 
146
- The walk is narrowed by NAME rather than opened up wholesale: a downstream file counts only if it imports one of the names the barrel republishes from that file (aliases followed, `export *` expanded to the file's own exports, type-only re-exports ignored they publish nothing at runtime). Without that narrowing, `export *` on a 40-entry barrel would credit its entire readership to every entry and `ui/orphaned` would quietly stop finding anything — trading a visible false positive for a silent false negative. There is a test for exactly that direction: a barrel with a used entry and a dead one must still report the dead one.
135
+ Postgres already had the unwrap, with a comment describing this exact consequence, and the three sibling dialects kept the broken call so the fix is now one shared `settleTransactionExit` in `@voltro/database` that all four import, plus a parity test that fails if any store's `transactional()` reaches `runtime.runPromise` again. Reported by a consumer on MariaDB who verified it against 0.19.0 too, so it is not a 0.20.0 regression.
147
136
 
148
137
  ### Internal (no consumer-facing effect)
149
138
 
150
- - **@voltro/protocol** — **`PluginHttpRouteRequest.store` names all four absences instead of one.** The docstring said "It is not tenant-scoped" and left soft-delete filtering, audit stamping and row-level security to be inferred from "everything that does not need a Subject".
151
-
152
- A team planning to port 19 raw-SQL sites onto that seam inferred the opposite: they wrote down "a store read adds `deletedAt IS NULL`" as the trap with teeth on their list — a soft-deleted user logging back in would go from "found and revived" to "not found → insert → unique violation on email" — and deferred the whole port partly over it. The store does no such thing; it is the raw store plus the storage codec. Naming exactly one of four absences reads as an exhaustive list.
153
-
154
- The docstring now carries the same table the `AuthStrategyInput.store` docs do, with the soft-delete row called out for anyone porting: a read here returns tombstones the way their SQL did, so a lookup that must see one needs no opt-out. (`.withDeleted()` is the opt-out on `ctx.store`, which *does* apply the filter.) Doc-only; the behaviour is unchanged and was already correct.
155
- - **`ci.yml` gains a `workflow_dispatch` trigger.** The full matrix — 11 database services plus the SQL Server AG init containers — is not reachable from a push to `main`: `paths-ignore` plus the job-level `if: github.event_name != 'push'` mean a main push runs static checks only.
139
+ - **@voltro/runtime, @voltro/database, @voltro/protocol, @voltro/cli, @voltro/plugin-billing, @voltro/plugin-mail, @voltro/plugin-sso-saml, @voltro/plugin-storage** — Fourteen source files carried a LITERAL NUL byte — the house idiom for a composite map key, written as the raw character instead of an escape. That makes the file BINARY to every text tool: `grep` skips it entirely and reports nothing, which is indistinguishable from a clean file. It was found because a new guard test scanning for framework index names came back clean on `runtime/src/connectionVault.ts` — 1020 lines that every previous grep-based audit in this repo had also silently skipped, including the one looking for exactly the index name that file declares.
156
140
 
157
- So the only ways to exercise it were a pull request and the release gate, which meant a change to the workflow itself could sit unrun until it fired for the first time INSIDE a release where a failure costs a ~40-minute round-trip and blocks the publish. That is precisely the position this repo was in.
141
+ Replaced with the JavaScript escape for U+0000. Identical runtime value, files are text again. No behaviour change.
@@ -11,16 +11,16 @@
11
11
  "dependencies": {
12
12
  "@effect/platform": "^0.96.1",
13
13
  "@effect/rpc": "^0.75.1",
14
- "@voltro/ai": "0.19.0",
15
- "@voltro/cli": "0.19.0",
16
- "@voltro/database": "0.19.0",
17
- "@voltro/env": "0.19.0",
18
- "@voltro/protocol": "0.19.0",
19
- "@voltro/runtime": "0.19.0",
14
+ "@voltro/ai": "0.20.1",
15
+ "@voltro/cli": "0.20.1",
16
+ "@voltro/database": "0.20.1",
17
+ "@voltro/env": "0.20.1",
18
+ "@voltro/protocol": "0.20.1",
19
+ "@voltro/runtime": "0.20.1",
20
20
  "effect": "^3.21.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@voltro/testing": "0.19.0",
23
+ "@voltro/testing": "0.20.1",
24
24
  "typescript": "^5.7.0",
25
25
  "vitest": "^3.0.0"
26
26
  }
@@ -12,17 +12,17 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.19.0",
16
- "@voltro/database": "0.19.0",
17
- "@voltro/env": "0.19.0",
18
- "@voltro/plugin-auth": "0.19.0",
19
- "@voltro/protocol": "0.19.0",
20
- "@voltro/runtime": "0.19.0",
21
- "@voltro/sql-postgres": "0.19.0",
15
+ "@voltro/cli": "0.20.1",
16
+ "@voltro/database": "0.20.1",
17
+ "@voltro/env": "0.20.1",
18
+ "@voltro/plugin-auth": "0.20.1",
19
+ "@voltro/protocol": "0.20.1",
20
+ "@voltro/runtime": "0.20.1",
21
+ "@voltro/sql-postgres": "0.20.1",
22
22
  "effect": "^3.21.2"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.19.0",
25
+ "@voltro/testing": "0.20.1",
26
26
  "typescript": "^5.7.0",
27
27
  "vitest": "^3.0.0"
28
28
  }
@@ -12,16 +12,16 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.19.0",
16
- "@voltro/database": "0.19.0",
17
- "@voltro/env": "0.19.0",
18
- "@voltro/plugin-multitenancy": "0.19.0",
19
- "@voltro/protocol": "0.19.0",
20
- "@voltro/runtime": "0.19.0",
15
+ "@voltro/cli": "0.20.1",
16
+ "@voltro/database": "0.20.1",
17
+ "@voltro/env": "0.20.1",
18
+ "@voltro/plugin-multitenancy": "0.20.1",
19
+ "@voltro/protocol": "0.20.1",
20
+ "@voltro/runtime": "0.20.1",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.19.0",
24
+ "@voltro/testing": "0.20.1",
25
25
  "typescript": "^5.7.0",
26
26
  "vitest": "^3.0.0"
27
27
  }
@@ -12,16 +12,16 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.19.0",
16
- "@voltro/database": "0.19.0",
17
- "@voltro/env": "0.19.0",
18
- "@voltro/plugin-deactivation": "0.19.0",
19
- "@voltro/protocol": "0.19.0",
20
- "@voltro/runtime": "0.19.0",
15
+ "@voltro/cli": "0.20.1",
16
+ "@voltro/database": "0.20.1",
17
+ "@voltro/env": "0.20.1",
18
+ "@voltro/plugin-deactivation": "0.20.1",
19
+ "@voltro/protocol": "0.20.1",
20
+ "@voltro/runtime": "0.20.1",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.19.0",
24
+ "@voltro/testing": "0.20.1",
25
25
  "typescript": "^5.7.0",
26
26
  "vitest": "^3.0.0"
27
27
  }
@@ -12,18 +12,18 @@
12
12
  "dependencies": {
13
13
  "@react-email/components": "^1.0.12",
14
14
  "@react-email/render": "^1.4.0",
15
- "@voltro/cli": "0.19.0",
16
- "@voltro/database": "0.19.0",
17
- "@voltro/env": "0.19.0",
18
- "@voltro/plugin-mail": "0.19.0",
19
- "@voltro/plugin-multitenancy": "0.19.0",
20
- "@voltro/protocol": "0.19.0",
21
- "@voltro/runtime": "0.19.0",
15
+ "@voltro/cli": "0.20.1",
16
+ "@voltro/database": "0.20.1",
17
+ "@voltro/env": "0.20.1",
18
+ "@voltro/plugin-mail": "0.20.1",
19
+ "@voltro/plugin-multitenancy": "0.20.1",
20
+ "@voltro/protocol": "0.20.1",
21
+ "@voltro/runtime": "0.20.1",
22
22
  "effect": "^3.21.2",
23
23
  "react": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.19.0",
26
+ "@voltro/testing": "0.20.1",
27
27
  "typescript": "^5.7.0",
28
28
  "vitest": "^3.0.0"
29
29
  }
@@ -12,18 +12,18 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.19.0",
16
- "@voltro/database": "0.19.0",
17
- "@voltro/env": "0.19.0",
18
- "@voltro/plugin-multitenancy": "0.19.0",
19
- "@voltro/plugin-storage": "0.19.0",
20
- "@voltro/protocol": "0.19.0",
21
- "@voltro/runtime": "0.19.0",
22
- "@voltro/sql-mysql": "0.19.0",
15
+ "@voltro/cli": "0.20.1",
16
+ "@voltro/database": "0.20.1",
17
+ "@voltro/env": "0.20.1",
18
+ "@voltro/plugin-multitenancy": "0.20.1",
19
+ "@voltro/plugin-storage": "0.20.1",
20
+ "@voltro/protocol": "0.20.1",
21
+ "@voltro/runtime": "0.20.1",
22
+ "@voltro/sql-mysql": "0.20.1",
23
23
  "effect": "^3.21.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.19.0",
26
+ "@voltro/testing": "0.20.1",
27
27
  "typescript": "^5.7.0",
28
28
  "vitest": "^3.0.0"
29
29
  }
@@ -10,17 +10,17 @@
10
10
  "test": "voltro test"
11
11
  },
12
12
  "dependencies": {
13
- "@voltro/cli": "0.19.0",
14
- "@voltro/database": "0.19.0",
15
- "@voltro/env": "0.19.0",
16
- "@voltro/plugin-multitenancy": "0.19.0",
17
- "@voltro/plugin-storage": "0.19.0",
18
- "@voltro/protocol": "0.19.0",
19
- "@voltro/runtime": "0.19.0",
13
+ "@voltro/cli": "0.20.1",
14
+ "@voltro/database": "0.20.1",
15
+ "@voltro/env": "0.20.1",
16
+ "@voltro/plugin-multitenancy": "0.20.1",
17
+ "@voltro/plugin-storage": "0.20.1",
18
+ "@voltro/protocol": "0.20.1",
19
+ "@voltro/runtime": "0.20.1",
20
20
  "effect": "^3.21.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@voltro/testing": "0.19.0",
23
+ "@voltro/testing": "0.20.1",
24
24
  "typescript": "^5.7.0",
25
25
  "vitest": "^3.0.0"
26
26
  }
@@ -11,17 +11,17 @@
11
11
  "dependencies": {
12
12
  "@effect/platform": "^0.96.1",
13
13
  "@effect/rpc": "^0.75.1",
14
- "@voltro/cli": "0.19.0",
15
- "@voltro/database": "0.19.0",
16
- "@voltro/env": "0.19.0",
17
- "@voltro/plugin-governance": "0.19.0",
18
- "@voltro/plugin-multitenancy": "0.19.0",
19
- "@voltro/protocol": "0.19.0",
20
- "@voltro/runtime": "0.19.0",
14
+ "@voltro/cli": "0.20.1",
15
+ "@voltro/database": "0.20.1",
16
+ "@voltro/env": "0.20.1",
17
+ "@voltro/plugin-governance": "0.20.1",
18
+ "@voltro/plugin-multitenancy": "0.20.1",
19
+ "@voltro/protocol": "0.20.1",
20
+ "@voltro/runtime": "0.20.1",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.19.0",
24
+ "@voltro/testing": "0.20.1",
25
25
  "typescript": "^5.7.0",
26
26
  "vitest": "^3.0.0"
27
27
  }