@voltro/cli 0.20.0 → 0.20.2

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 (59) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/dist/apiBuild-C9aHz4Yh.js +2 -0
  3. package/dist/{apiBuild-CSCVI1wC.js → apiBuild-qZBEu59d.js} +2 -2
  4. package/dist/bin.js +41 -9
  5. package/dist/{commands-DQ0mUwDe.js → commands-kYbfVoPO.js} +1657 -1593
  6. package/dist/{dbCommand-DJSxnjPt.js → dbCommand-BPp9R0Wc.js} +25 -25
  7. package/dist/dbCommand-DtUe-0dM.js +2 -0
  8. package/dist/{dev-Z1E_twG9.js → dev-2AvdzDg2.js} +1 -1
  9. package/dist/{dev-DnHlqqoF.js → dev-OHLsAq4K.js} +1638 -1621
  10. package/dist/index.js +1 -1
  11. package/dist/{inspectMetrics-BL8kZOv3.js → inspectMetrics-DvPNXmGA.js} +12 -3
  12. package/dist/{serveCommand-Dh4qd49N.js → serveCommand-BKqTKWTX.js} +341 -341
  13. package/dist/serveEntry.js +2 -2
  14. package/dist/{start-IVgQe2YD.js → start-CXQ7WL1W.js} +10 -2
  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/seedsdialects.md +25 -0
  21. package/templates/agent-docs/database/transactions.md +15 -0
  22. package/templates/agent-docs/whats-new.md +9 -74
  23. package/templates/apps/api-ai/package.json +7 -7
  24. package/templates/apps/api-auth/package.json +8 -8
  25. package/templates/apps/api-backend/package.json +7 -7
  26. package/templates/apps/api-backend-deactivation/package.json +7 -7
  27. package/templates/apps/api-backend-mail/package.json +8 -8
  28. package/templates/apps/api-backend-mariadb/package.json +9 -9
  29. package/templates/apps/api-backend-storage/package.json +8 -8
  30. package/templates/apps/api-data-advanced/package.json +8 -8
  31. package/templates/apps/api-durable/package.json +8 -8
  32. package/templates/apps/api-feature-flags/package.json +9 -9
  33. package/templates/apps/api-governance/package.json +8 -8
  34. package/templates/apps/api-kv/package.json +8 -8
  35. package/templates/apps/api-moderation/package.json +8 -8
  36. package/templates/apps/api-observability/package.json +8 -8
  37. package/templates/apps/api-ratelimit/package.json +8 -8
  38. package/templates/apps/api-rbac/package.json +8 -8
  39. package/templates/apps/api-rest/package.json +7 -7
  40. package/templates/apps/api-saas/package.json +11 -11
  41. package/templates/apps/api-search/package.json +8 -8
  42. package/templates/apps/api-versioning/package.json +8 -8
  43. package/templates/apps/api-webhooks/package.json +9 -9
  44. package/templates/apps/changelog/package.json +6 -6
  45. package/templates/apps/edge-functions/package.json +2 -2
  46. package/templates/apps/frontend-admin/package.json +8 -8
  47. package/templates/apps/frontend-app/package.json +8 -8
  48. package/templates/apps/frontend-blank/package.json +7 -7
  49. package/templates/apps/frontend-contact/package.json +7 -7
  50. package/templates/apps/frontend-dashboard/package.json +7 -7
  51. package/templates/apps/frontend-docs/package.json +7 -7
  52. package/templates/apps/frontend-i18n/package.json +6 -6
  53. package/templates/apps/frontend-landing/package.json +7 -7
  54. package/templates/apps/frontend-spa/package.json +7 -7
  55. package/templates/apps/frontend-ssr/package.json +7 -7
  56. package/templates/apps/frontend-ssr-api/package.json +8 -8
  57. package/templates/apps/frontend-static-blog/package.json +6 -6
  58. package/dist/apiBuild-B1FDtx1y.js +0 -2
  59. package/dist/dbCommand-C3R5LrBZ.js +0 -2
@@ -1,5 +1,5 @@
1
- import { et as e } from "./inspectMetrics-BL8kZOv3.js";
1
+ import { et as e } from "./inspectMetrics-DvPNXmGA.js";
2
2
  import { c as t } from "./seedRunner-D6eu-u5U.js";
3
3
  import { r as n } from "./appModuleLoader-C9r9mxZt.js";
4
- import { t as r } from "./serveCommand-Dh4qd49N.js";
4
+ import { t as r } from "./serveCommand-BKqTKWTX.js";
5
5
  export { e as loadDotEnv, n as registerAppModules, t as registerDriver, r as runServe };
@@ -1,4 +1,4 @@
1
- import { A as e, C as t, E as n, F as r, G as i, H as a, J as o, K as s, O as c, Q as l, R as u, Y as ee, Z as d, _ as te, a as f, at as p, b as m, c as h, f as g, ft as _, g as v, h as y, i as b, it as ne, j as x, k as re, lt as S, m as C, nt as ie, o as w, ot as T, p as E, q as D, r as O, rt as k, s as A, t as j, v as M, w as ae, y as N } from "./inspectMetrics-BL8kZOv3.js";
1
+ import { A as e, C as t, E as n, F as r, G as i, H as a, J as o, K as s, O as c, Q as l, R as u, Y as ee, Z as d, _ as te, a as f, at as p, b as m, c as h, f as g, ft as _, g as v, h as y, i as b, it as ne, j as x, k as re, lt as S, m as C, nt as ie, o as w, ot as T, p as E, q as D, r as O, rt as k, s as A, t as j, v as M, w as ae, y as N } from "./inspectMetrics-DvPNXmGA.js";
2
2
  import { D as oe, E as se, T as ce, a as P, p as le, w as ue } from "./inspect-Dwx0_tUj.js";
3
3
  import { t as de } from "./bootTiming-BdyP9nYw.js";
4
4
  import { dirname as fe, extname as F, join as I, resolve as L } from "node:path";
@@ -407,7 +407,15 @@ CREATE INDEX IF NOT EXISTS ${e}_servable_until_idx ON ${e} (servable_until);
407
407
  },
408
408
  appType: "custom",
409
409
  logLevel: "warn"
410
- }), o = I(e, "src", "pages"), s = (e) => e === "" ? o : I(o, e), c = d({ limit: l() }), u = (e) => c.load(e, async () => await a.ssrLoadModule(e));
410
+ }), o = I(e, "src", "pages"), s = (e) => e === "" ? o : I(o, e), c = d({
411
+ limit: l(),
412
+ onColdStart: (e) => q.debug("ssr cold-compile start", { id: e }),
413
+ onColdEnd: (e, { durationMs: t, failed: n }) => q.debug(`ssr cold-compile ${n ? "FAILED" : "end"} ${t}ms`, {
414
+ id: e,
415
+ ms: t,
416
+ ...n ? { failed: !0 } : {}
417
+ })
418
+ }), u = (e) => c.load(e, async () => await a.ssrLoadModule(e));
411
419
  return {
412
420
  kind: "middleware",
413
421
  async getPage(e, t) {
@@ -1,3 +1,3 @@
1
- import { et as e } from "./inspectMetrics-BL8kZOv3.js";
2
- import { t } from "./start-IVgQe2YD.js";
1
+ import { et as e } from "./inspectMetrics-DvPNXmGA.js";
2
+ import { t } from "./start-CXQ7WL1W.js";
3
3
  export { e as loadDotEnv, t as runStartCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -62,22 +62,22 @@
62
62
  "@effect/platform-node": "^0.107.0",
63
63
  "@effect/sql": "^0.51.1",
64
64
  "@effect/workflow": "^0.18.2",
65
- "@voltro/ai": "0.20.0",
66
- "@voltro/cache": "0.20.0",
67
- "@voltro/data-transfer": "0.20.0",
68
- "@voltro/database": "0.20.0",
69
- "@voltro/env": "0.20.0",
70
- "@voltro/kv": "0.20.0",
71
- "@voltro/logger": "0.20.0",
72
- "@voltro/plugin-auth": "0.20.0",
73
- "@voltro/plugin-broadcast": "0.20.0",
74
- "@voltro/plugin-mail": "0.20.0",
75
- "@voltro/plugin-storage": "0.20.0",
76
- "@voltro/plugin-webhooks": "0.20.0",
77
- "@voltro/protocol": "0.20.0",
78
- "@voltro/runtime": "0.20.0",
79
- "@voltro/serverless": "0.20.0",
80
- "@voltro/workflow": "0.20.0",
65
+ "@voltro/ai": "0.20.2",
66
+ "@voltro/cache": "0.20.2",
67
+ "@voltro/data-transfer": "0.20.2",
68
+ "@voltro/database": "0.20.2",
69
+ "@voltro/env": "0.20.2",
70
+ "@voltro/kv": "0.20.2",
71
+ "@voltro/logger": "0.20.2",
72
+ "@voltro/plugin-auth": "0.20.2",
73
+ "@voltro/plugin-broadcast": "0.20.2",
74
+ "@voltro/plugin-mail": "0.20.2",
75
+ "@voltro/plugin-storage": "0.20.2",
76
+ "@voltro/plugin-webhooks": "0.20.2",
77
+ "@voltro/protocol": "0.20.2",
78
+ "@voltro/runtime": "0.20.2",
79
+ "@voltro/serverless": "0.20.2",
80
+ "@voltro/workflow": "0.20.2",
81
81
  "chokidar": "^5.0.0",
82
82
  "ioredis": "^5.11.1",
83
83
  "tinyglobby": "^0.2.17",
@@ -595,7 +595,7 @@ each plugin's own README.
595
595
 
596
596
  | Topic | Open | Summary |
597
597
  |---|---|---|
598
- | **What's new in 0.20.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
598
+ | **What's new in 0.20.2** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
599
599
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
600
600
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
601
601
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -9,7 +9,7 @@ each plugin's own README.
9
9
 
10
10
  | Topic | Open | Summary |
11
11
  |---|---|---|
12
- | **What's new in 0.20.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
12
+ | **What's new in 0.20.2** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
13
13
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
14
14
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
15
15
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -545,6 +545,21 @@ fully concurrent. Tune the cap with `VOLTRO_DEV_SSR_COMPILE_CONCURRENCY` (below)
545
545
  drop it on a low-memory box, raise it on a big machine.
546
546
 
547
547
 
548
+ Under `VOLTRO_LOG_LEVEL=debug` each cold compile logs its own duration, so a slow
549
+ first paint can be attributed to a specific module:
550
+
551
+ ```
552
+ [voltro:dev:web] ssr cold-compile start id=/app/src/pages/layout.tsx
553
+ [voltro:dev:web] ssr cold-compile start id=/app/src/pages/(main)/layout.tsx
554
+ [voltro:dev:web] ssr cold-compile end 3743ms id=/app/src/pages/layout.tsx
555
+ ```
556
+
557
+ Read the `ms` on the **end** line rather than subtracting timestamps: compiles run
558
+ concurrently up to the cap, so the start and end lines interleave and adjacent
559
+ lines usually belong to different modules. The duration is measured inside the
560
+ concurrency permit, so it is that module's own compile cost and not time spent
561
+ queued behind the cap. A compile that threw says `FAILED` instead of `end`.
562
+
548
563
  ### Fast Refresh: what hot-updates and what reloads
549
564
 
550
565
  Editing a **page or layout component** applies as a hot update — the React tree
@@ -835,6 +850,24 @@ apps/acme/web/.framework/dist/
835
850
  └── ssrEntry.js # SSR bundle for voltro start
836
851
  ```
837
852
 
853
+ ## Unresolvable optional peers
854
+
855
+ The SSR bundle inlines everything it reaches (`ssr: { noExternal: true }`), which is what lets a production web image ship without a framework dependency tree. A package that cannot be **resolved at all** is externalised instead of failing the build — almost always an uninstalled optional native peer reached through a library's Node entry point:
856
+
857
+ ```
858
+ Rolldown failed to resolve import "canvas" from ".../konva/lib/index-node.js"
859
+ ```
860
+
861
+ `konva`'s `main` is its Node build, which requires the optional `canvas`; its `browser` field points at one that does not. An app that never renders to a canvas server-side has nothing to install, and there is no app-side workaround: making the import dynamic does not help (the bundler must still resolve it to form the chunk), and `renderMode: 'spa'` does not either — the generated router imports every page statically, so the module is in the SSR graph whatever the render mode.
862
+
863
+ Every specifier externalised this way is named on the success line:
864
+
865
+ ```
866
+ [voltro:build] SSR bundle ready { path: 'dist/server/ssrEntry.js', externalizedOptionalPeers: 'canvas' }
867
+ ```
868
+
869
+ Read that list. Externalising is right for an optional peer you never use, and wrong for a dependency you forgot to install — it turns a build failure into a runtime one, and only you can tell the two apart. Framework packages (`@voltro/*`, `@effect/*`, `effect`) are never externalised.
870
+
838
871
  ## `voltro start <appDir>`
839
872
 
840
873
  ```bash
@@ -802,6 +802,31 @@ Each replica persists its progress in the **`_voltro_cdc_offsets`** table — on
802
802
 
803
803
  If the persisted offset has been purged (`err 1236`) or rejected after a failover, the reader jumps to the current binlog end and signals a resync so dependent subscriptions re-query rather than missing the gap.
804
804
 
805
+ ### A table the reader cannot decode
806
+
807
+ `@vlasky/zongji` reads each event's column layout from the binlog's `Table_map` (fixed on disk) and compares it to a fresh `information_schema` fetch. A mismatch throws:
808
+
809
+ ```
810
+ Table app.sessions schema changed between binlog event and metadata fetch:
811
+ the event has 9 columns, fetched metadata has 8
812
+ ```
813
+
814
+ The usual cause on MariaDB is a **UNIQUE constraint on an unbounded text column**. MariaDB can only back that with a **HASH long-unique index**, and that index adds a hidden `DB_ROW_HASH_n` column to the InnoDB row — present in the binlog row image, absent from `information_schema.COLUMNS`. So the counts can never agree, and every write to that table trips it. Nothing is broken; the table is shaped that way.
815
+
816
+ The reader finds such tables when CDC starts, reports each once, and **excludes** it — so there is no reconnect loop. Cross-instance change events for that table are lost; own-node reactivity is unaffected, because writes still emit inline.
817
+
818
+ **The remedy is to bound the column:**
819
+
820
+ ```ts
821
+ tokenHash: text().maxLength(64).unique() // VARCHAR(64) → ordinary B-tree index
822
+ ```
823
+
824
+ `ALTER TABLE … FORCE` does **not** help. The rebuild recreates the index and therefore recreates the hidden column — measured before and after: same column count both times. If you already tried it, that was not your mistake.
825
+
826
+ A bounded unique is worth having anyway: it is also what keeps the key inside the index-size limits on every dialect.
827
+
828
+ The other cause of the same message is a **backlog event that predates a migration** — the reader was down while a table changed. That one is unreplayable but transient: the reader skips to the current binlog end, signals a resync, and recovers. One warning, then it is over.
829
+
805
830
  ### Boot summary
806
831
 
807
832
  ```
@@ -217,6 +217,21 @@ Common uses:
217
217
 
218
218
  Returns the FINAL row in both cases (newly-inserted or pre-existing).
219
219
 
220
+ ### One conflict target, and only conflicts
221
+
222
+ `conflictColumns` names ONE constraint. A duplicate on a *different* unique index is not something `insertIgnore` can resolve — it cannot know which existing row you meant — so it throws, naming the constraint that actually fired.
223
+
224
+ It also throws when the insert was **rejected** rather than skipped. This matters most on MariaDB, where the statement lowers to `INSERT IGNORE`: that downgrades *every* error to a warning — foreign key, NOT NULL, CHECK, truncation — not just the unique violation the API models. So "no row was inserted" does not imply "a conflict happened", and reporting one as the other would turn a rejected write into a silent no-op: the row is not there and the caller is told it already was.
225
+
226
+ ```
227
+ MysqlStore.insertIgnore: the insert into 'docs' was REJECTED, not skipped as a
228
+ conflict. INSERT IGNORE downgrades every error to a warning, and the warning was:
229
+ [1452] Cannot add or update a child row: a foreign key constraint fails …
230
+ Nothing was written and nothing conflicted — fix the cause above.
231
+ ```
232
+
233
+ The real cause comes from `SHOW WARNINGS` on the same connection, which is only attributable inside a transaction — so outside one the message says the constraint is unknown rather than guessing at it. Framework mutations are auto-transactional, so the common path has the cause.
234
+
220
235
  ## `updateMany` — one-statement bulk update
221
236
 
222
237
  ```ts
@@ -1,4 +1,4 @@
1
- # What's new in 0.20.0
1
+ # What's new in 0.20.2
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,85 +7,20 @@ 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
11
-
12
- - **@voltro/plugin-versioning, @voltro/database, @voltro/voltro, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql** — `versioningPlugin({ timing: 'in-transaction' })` produced a WRONG trail, not merely a slow one. Reported and reproduced against MariaDB 11 by a team that wired both plugins and measured before migrating a single call site.
13
-
14
- **It recorded every change twice.** The two timings are alternatives, but the post-commit change tap stayed wired when the in-transaction recorder was registered, so both ran. One `bookmarks.create` → two history rows.
15
-
16
- **And the trail was mis-ordered, which is worse.** Each path numbered independently: one insert plus one update produced versions `0, 0, 1, 2` across four rows. `selectAsOf`, `sortHistory` and `diffVersionRows` all read `version`, so `rowAsOf` returned the wrong snapshot and `diffVersions` found nothing. A duplicate can be deduped; a wrong order cannot be detected from the data.
17
-
18
- The recorder wrote a constant `version: 0` on purpose, with a design note arguing that ordering could come from `changedAt` and that a read per covered write was too expensive. Both halves were wrong: `changedAt` is millisecond-resolution, so two writes to one row inside one transaction tie routinely, and the number is what every reader consults.
19
-
20
- **BREAKING —** a `WriteRecorder` now receives a PORT (`{ append, maxOf }`) rather than a bare `append`. `maxOf` is one aggregate with an equality filter on the connection the write already holds; it is what lets an append-only trail number its own entries. A recorder still cannot UPDATE, DELETE or open a nested transaction, and a throw from either operation still rolls the caller's write back. Apps that merely ENABLE the timing need no change — only a hand-written recorder does, and `tsc` names every site.
21
-
22
- **Cost, stated rather than avoided:** `'in-transaction'` now takes TWO round-trips per recorded write, roughly doubling this timing's published per-write overhead. Both timings number from 1, so switching `timing` no longer shifts version numbers.
23
-
24
10
  ### Fixed
25
11
 
26
- - **@voltro/database, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql** — The correlation bridge did not survive a transaction, and did not survive CDC. Both are fixed, and both were found by measuring against live databases after a consumer isolated the symptom in a scratch app.
27
-
28
- **Every write a framework mutation makes was unattributed.** `transactional()` is entered from the request's async-local scope, but its callback runs from inside the Effect the store builds — and measured against live postgres AND live mariadb, the scope is active at the call site and EMPTY inside the callback. Framework mutations are auto-transactional, so this was every handler write. Same class as the `bindMutation` defect fixed alongside it: a scope covering the construction of an Effect and not its execution. The caller's attribution is now captured at `transactional()` entry and re-established around the callback, in all four dialect stores.
29
-
30
- **And the CDC transports could not carry it at all.** Under `changeStrategy: 'cdc'` — the DEFAULT — the event a subscriber receives is rebuilt from a postgres NOTIFY payload or a mysql binlog row image, neither of which can hold a request context. `registerPendingAttribution` / `claimPendingAttribution` (`@voltro/database`) let the write path hand its identity to the echo, keyed by `(table, op, id)` and claimed once. A write made on ANOTHER replica has nothing pending and stays unattributed, which is the correct answer rather than a gap.
31
-
32
- **Plus one nobody had reported, found on the way:** on postgres under CDC the write path skipped `routeEvent` entirely, and `runWriteRecorders` lives inside it — so `versioningPlugin({ timing: 'in-transaction' })` with the default `CDC=1` recorded NOTHING. The mode whose entire promise is "if the change committed, the entry is there" wrote an empty trail, silently. `routeEvent` now runs in both modes; only the DELIVERY decision is strategy-dependent.
33
-
34
- New live-dialect suites (`cdcAttribution.integration.test.ts` in `sql-postgres` and `sql-mysql`) pin all of it, and were verified red against the previous code.
35
- - **@voltro/plugin-versioning, @voltro/runtime, @voltro/cli** — `_voltro_row_history.traceId` and `.subjectId` were NULL on every write. Three independent causes, all found from one consumer report whose evidence pinned the diagnosis before we looked: `subjectId` was NULL while `changedBy` on the SAME row carried the acting user — so the identity was known and was not travelling.
36
-
37
- - **The adapter dropped them.** `dataStoreHistoryStore.append` hand-wrote its insert object and listed `changedBy` but not `traceId` / `subjectId`. This is the second time that shape has bitten in this file — the READ side (`rowToVersion`) had drifted identically. A row built by hand in one place and read by hand in another disagree exactly when a field is ADDED, because nothing fails. Both now spread the row. - **An Effect-returning handler was unattributed.** `bindMutation` established the scope around the CALL, which covers an async executor for its whole run — but an Effect-returning one is only CONSTRUCTED there and runs later. It is now forked inside the scope, with interruption and typed failures preserved (both pinned by tests). Verified by measurement, not assumption: an Effect forked inside an ALS scope keeps seeing it across `sleep`, `yieldNow` and a `setTimeout` promise, while the same effect merely constructed inside sees nothing. - **The devtools `/invoke` path never entered the scope at all.** It bypasses the rpc stack by design, and that also bypassed everything `bindMutation` sets up. The audit plugin recorded a traceId (it reads `requestContext.traceId`, which this path does build) while every write underneath carried none — three consumers of one call disagreeing about its trace. It also synthesised `inspect-<8 random chars>`, which no trace consumer can parse; the reporter's framing is the rule worth keeping — *a synthesised id produces a column that looks joinable and is not; NULL at least fails honestly.* It is a real 32-hex id now, and it reaches all three sinks.
38
-
39
- `actingUserId` is imported at the new call site rather than re-derived — one answer to "who is writing", shared with what `audit()` stamps.
40
- - **@voltro/cli** — Three ways a check reported nothing while checking nothing, all found by a consumer verifying the silence instead of trusting it.
41
-
42
- - **`unexercised-row-filter` never fired on a typed registration.** The match was `/\bsetRowFilter\s*\(/`, which demands the paren directly after the name, so `setRowFilter<Ctx>({…})` — the spelling our own generic signature invites — broke it. The rule was blind for exactly the teams that had wired `load`/`predicate` carefully. It counts CALLS now. - **…and its test-side condition was satisfiable by a COMMENT.** It matched `rowFilter:` in raw text. Comments and string literals are stripped, and the suite must both call `makeTestContext` and bind `rowFilter` in real code. - **The same paren-adjacent shape sat in two shipped codemod gates.** `0.7.0/01` (row filter) and `0.7.0/02` (`invoke`) both gate on a generic export, so a typed call made `voltro update` print nothing at all: the upgrade reads as clean and the behaviour change lands unread. Both now use the shared `callPattern`, which allows type arguments including nested ones.
43
-
44
- Two false-positive fixes in the hand-roll detector, from the same report:
45
-
46
- - **A file that WIRES a plugin is no longer told to adopt it.** The `presence` rule reported `app.config.ts` (which calls `presencePlugin()`) to an app that had just deleted its hand-rolled table. Rules that recommend a package now declare it, and a file referencing that package is skipped. - **Generated files and `.d.ts` are out of the scan.** A recommendation aimed at a file the next boot overwrites is never actionable.
47
-
48
- And one more of the first kind, found while checking why a withdrawn report's probe had stayed silent: `raw-fetch` counted only a BARE `fetch(…)` callee, so `globalThis.fetch(url)` / `self.fetch(url)` in a server file read as clean.
49
- - **@voltro/cli** — `voltro doctor`'s `serverOnly: NOT CHECKED` line now names the failure, and the field exists in `--json`.
50
-
51
- The refusal to claim a pass was right. What shipped with it was nothing to act on: the `catch` discarded the error entirely, so there was no reason, no failing module, and — because the field was absent from `--json` — no way for CI to assert "still unchecked" rather than reading silence as a pass.
52
-
53
- A consumer's verdict, which is the useful part: *"The message is honest and that is the problem."* They had already verified that every descriptor, `app.config.ts` and the generated rpc group imported cleanly under `tsx` on their own, so the difference had to be in what `loadDiscovered` does BEYOND importing — and none of that was visible from outside. It matters more than its size because `.serverOnly()` is what guards their `sessions.tokenHash` and `apiKeys.keyHash`, markers they added after finding a query whose output schema shipped a hash over the wire.
54
-
55
- `--json` now carries `serverOnly: { checked, reason?, leaks? }`. Gate CI on `checked === false`.
56
- - **@voltro/plugin-versioning** — A version snapshot no longer copies `.serverOnly()` columns into `_voltro_row_history`. `.encrypted()` columns are KEPT, and that distinction is the whole finding.
57
-
58
- Reported by a team choosing which tables to version: `sessions` holds `.encrypted()` PATs and a `tokenHash`, `apiKeys` holds a `keyHash`, and they could not determine from outside what the snapshot would contain. They excluded both tables — then went and measured it, which corrected their own report:
59
-
60
- ```
61
- probeItems.secret enc:v1:a56iziEV9THLhzmJ:Vk0ux+0bECleTLBJkCa0Rg==:3AtMwP…
62
- _voltro_row_history {"secret":"enc:v1:a56iziEV9THLhzmJ:Vk0ux+0bECleTLBJkCa0Rg==:…"}
63
- ```
64
-
65
- **`.encrypted()` lands as ciphertext, byte-identical to the source column**, so versioning such a table widens nothing — the history is exactly as readable as the row it came from. Withholding it would have cost real audit data to prevent an exposure that does not exist.
66
-
67
- **`.serverOnly()` is withheld**, and the reason is not "a second copy under different retention" — that argument is weak on its own, since the hash already sits in the source table. The decisive one: `crud.*` STRIPS `.serverOnly()` columns from every row it returns, and a snapshot would smuggle the same value back past that stripping inside a `json()` blob, where no column-level rule applies.
68
-
69
- Withheld names are listed under `data._omitted`, so a reader can tell "this column was withheld" from "this column did not exist then". Both timings apply the same policy. `.sensitive()` is not involved: it is an export-masking marker for values that are legitimately readable in the app.
70
- - **@voltro/cli** — A page that RE-EXPORTS its component (`export { default, renderMode } from '../page'`) no longer fails the codegen gate with "exports no default". The check required the literal words `as default`, so the one spelling that lets two routes share a screen without copying it was the one spelling it refused — and it refused in `voltro build`, while dev and tests stayed green because nothing prerenders there. `export { default as Screen }` is still correctly rejected: it renames the default away.
71
-
72
- Two follow-ons from the same shape:
73
-
74
- - The refusal message said the file "ends in `.page.tsx`" and offered "drop the `.page` suffix" as a fix. That is the 0.15.0 convention, replaced by directory routing in 0.17.0 — it named a convention that no longer exists and a fix that could not work. It now names `page.tsx` and both real fixes. - `scanRenderProfile` read a forwarded `renderMode` as absent and fell back to `'static'`, so `staticSafe` and the deploy-target classification could call an app CDN-deployable with an `ssr` route in it. The forward is now followed (relative specifiers, depth-capped); an unresolvable one still falls back rather than failing the scan.
75
- - **@voltro/database** — `VOLTRO_SOFT_DROP=1` could never converge. The applier renames the object to `<name>__dropped_<ts>` instead of dropping it, which leaves it undeclared — and the differ read that as one more forgotten table, planning the drop again. The re-plan inside `applyPlan` then found an operation still outstanding and aborted with "the DDL for these operations is a no-op — this is a framework bug", which was a wrong diagnosis of a real defect: the DDL had worked. No fingerprint was recorded, so the migration counted as unapplied and every later `db apply` / boot hit the same wall. The only exit was a hard drop of the snapshot — exactly the recoverability the flag is chosen for.
76
-
77
- The planner now treats `<name>__dropped_<YYYYMMDDHHMMSS>` as framework-managed, alongside `_voltro_*` / `cluster_*`. Deliberately not retention-aware: a planner whose output depends on the clock would produce different plans before and after midnight, and `db gc-snapshots` already owns expiry.
12
+ - **@voltro/database, @voltro/cli, @voltro/voltro** — `voltro db drift` could never report clean. It compared the LIVE schema's fingerprint against `_voltro_migration_plans.fingerprint` which is the **declared** snapshot's hash. The two are not comparable: introspection cannot recover everything a declaration carries (generated expressions, `maxLength`, sensitivity markers), so hashing a live snapshot never equals hashing the declaration it came from. The command was therefore RED on a provably clean database, permanently.
78
13
 
79
- Reported against tables; the same defect existed one level down for soft-dropped COLUMNS, where it was worse a re-planned `drop-column` carries no `dropped()` marker and so refuses to plan at all. Both are fixed.
14
+ Fixing the postgres-only cast in the previous release is what made this visiblebefore that, `db drift` crashed before it could compute a wrong answer.
80
15
 
81
- The convergence message itself no longer asserts a cause it cannot know. It said "the DDL for these operations is a no-op", which was flatly wrong here and sent the reporter looking for dead DDL. It now names both causes — no-op DDL, and a planner that cannot see what the DDL did — and says which one an operation naming a just-renamed object usually is.
16
+ A consumer measured it precisely: three different fingerprints for one database (`apply` recorded `b1078b73…`, `drift` computed `8dcc9c5e…`, `plan` computed `0c376108…`), stable across runs, with `db plan` reporting 0 operations in between.
82
17
 
83
- ### Internal (no consumer-facing effect)
18
+ **The fix is a second, comparable baseline.** `_voltro_migration_plans` gains `liveFingerprint` — the post-apply LIVE fingerprint, taken from the convergence re-plan's `fromFingerprint` (that re-plan runs AFTER the apply, so its "pre-state" is our post-state; it is already computed, so this costs nothing). `db drift` compares against that, and hashes the live snapshot WHOLE, exactly as the applier did.
84
19
 
85
- - **The `0.20.0/01_write-recorder-port` codemod gains the gate test its two predecessors have.**
20
+ It used to strip `_voltro_*` tables before hashing, which sounds reasonable and was half the incomparability. A framework upgrade that adds a `_voltro_*` column will now show as drift until the next `db apply` records a new baseline — honest, since the live schema did change, and it self-heals on the apply an upgrade needs anyway.
86
21
 
87
- `codemodRegistry.test.ts` asserts that every `*.codemod.ts` on disk is registered and that ids are unique — registration, not behaviour. What it cannot see is the one way a `manual` codemod fails in practice: an `appliesTo` that is too broad, so the note prints for projects that have nothing to do. That is not a cosmetic problem. A note which fires on every app is how readers learn to skip notes, and the next one carries a boot refusal.
22
+ Rows written before the column exists have no baseline. `db drift` says so and exits 0, instead of comparing a live hash against a declared one and calling the difference drift. `_voltro_*` table changes ride the declarative differ, so no codemod.
88
23
 
89
- This codemod is the case where the silent direction matters most. The break is a TYPE error, so `tsc` already names every affected site; the note exists only to explain `maxOf`, which the compiler cannot. Apps that merely ENABLE `timing: 'in-transaction'` need to do nothing`plugin-versioning` ships the recorder and it is already updated — and they are the large majority.
24
+ **The "Probable causes" list is gone.** It named out-of-band DDL and a missing ledger row, and the consumer hit it with neither being true the row was right there in `db plans`. A diagnosis that asserts a cause it cannot know is the same defect as the `ALTER TABLE FORCE` repair line retracted in the same release, and it costs more here because it is confident: it sends the reader hunting through somebody's shell history. The command now says what it can actually see that the schema changed, not what or who — and points at `db plan` for the real difference.
90
25
 
91
- Four cases, covering both directions: a project registering its own recorder (note prints, and names `{ append }`, `maxOf`, and the `null`-is-not-zero distinction that a hand-written sequence gets wrong), an app that only enables the timing (silent), the identifier in a comment or a string (silent), and the generic call form `registerWriteRecorder<Row>(…)`, which `callPattern` admits and a naive match would miss.
26
+ Worth recording what the pair of defects cost together, in the consumer's framing: `db drift` exists to catch a divergence between declared and live, and the one real divergence they have (`sessions.tokenHash` declared `varchar(64)`, live `longtext`) is invisible to it while it loudly reported a divergence that did not exist. False negative on the real thing, false positive on nothing. The false negative is the still-open `maxLength`-in-the-snapshot item.
@@ -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.20.0",
15
- "@voltro/cli": "0.20.0",
16
- "@voltro/database": "0.20.0",
17
- "@voltro/env": "0.20.0",
18
- "@voltro/protocol": "0.20.0",
19
- "@voltro/runtime": "0.20.0",
14
+ "@voltro/ai": "0.20.2",
15
+ "@voltro/cli": "0.20.2",
16
+ "@voltro/database": "0.20.2",
17
+ "@voltro/env": "0.20.2",
18
+ "@voltro/protocol": "0.20.2",
19
+ "@voltro/runtime": "0.20.2",
20
20
  "effect": "^3.21.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@voltro/testing": "0.20.0",
23
+ "@voltro/testing": "0.20.2",
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.20.0",
16
- "@voltro/database": "0.20.0",
17
- "@voltro/env": "0.20.0",
18
- "@voltro/plugin-auth": "0.20.0",
19
- "@voltro/protocol": "0.20.0",
20
- "@voltro/runtime": "0.20.0",
21
- "@voltro/sql-postgres": "0.20.0",
15
+ "@voltro/cli": "0.20.2",
16
+ "@voltro/database": "0.20.2",
17
+ "@voltro/env": "0.20.2",
18
+ "@voltro/plugin-auth": "0.20.2",
19
+ "@voltro/protocol": "0.20.2",
20
+ "@voltro/runtime": "0.20.2",
21
+ "@voltro/sql-postgres": "0.20.2",
22
22
  "effect": "^3.21.2"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.20.0",
25
+ "@voltro/testing": "0.20.2",
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.20.0",
16
- "@voltro/database": "0.20.0",
17
- "@voltro/env": "0.20.0",
18
- "@voltro/plugin-multitenancy": "0.20.0",
19
- "@voltro/protocol": "0.20.0",
20
- "@voltro/runtime": "0.20.0",
15
+ "@voltro/cli": "0.20.2",
16
+ "@voltro/database": "0.20.2",
17
+ "@voltro/env": "0.20.2",
18
+ "@voltro/plugin-multitenancy": "0.20.2",
19
+ "@voltro/protocol": "0.20.2",
20
+ "@voltro/runtime": "0.20.2",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.20.0",
24
+ "@voltro/testing": "0.20.2",
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.20.0",
16
- "@voltro/database": "0.20.0",
17
- "@voltro/env": "0.20.0",
18
- "@voltro/plugin-deactivation": "0.20.0",
19
- "@voltro/protocol": "0.20.0",
20
- "@voltro/runtime": "0.20.0",
15
+ "@voltro/cli": "0.20.2",
16
+ "@voltro/database": "0.20.2",
17
+ "@voltro/env": "0.20.2",
18
+ "@voltro/plugin-deactivation": "0.20.2",
19
+ "@voltro/protocol": "0.20.2",
20
+ "@voltro/runtime": "0.20.2",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.20.0",
24
+ "@voltro/testing": "0.20.2",
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.20.0",
16
- "@voltro/database": "0.20.0",
17
- "@voltro/env": "0.20.0",
18
- "@voltro/plugin-mail": "0.20.0",
19
- "@voltro/plugin-multitenancy": "0.20.0",
20
- "@voltro/protocol": "0.20.0",
21
- "@voltro/runtime": "0.20.0",
15
+ "@voltro/cli": "0.20.2",
16
+ "@voltro/database": "0.20.2",
17
+ "@voltro/env": "0.20.2",
18
+ "@voltro/plugin-mail": "0.20.2",
19
+ "@voltro/plugin-multitenancy": "0.20.2",
20
+ "@voltro/protocol": "0.20.2",
21
+ "@voltro/runtime": "0.20.2",
22
22
  "effect": "^3.21.2",
23
23
  "react": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.20.0",
26
+ "@voltro/testing": "0.20.2",
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.20.0",
16
- "@voltro/database": "0.20.0",
17
- "@voltro/env": "0.20.0",
18
- "@voltro/plugin-multitenancy": "0.20.0",
19
- "@voltro/plugin-storage": "0.20.0",
20
- "@voltro/protocol": "0.20.0",
21
- "@voltro/runtime": "0.20.0",
22
- "@voltro/sql-mysql": "0.20.0",
15
+ "@voltro/cli": "0.20.2",
16
+ "@voltro/database": "0.20.2",
17
+ "@voltro/env": "0.20.2",
18
+ "@voltro/plugin-multitenancy": "0.20.2",
19
+ "@voltro/plugin-storage": "0.20.2",
20
+ "@voltro/protocol": "0.20.2",
21
+ "@voltro/runtime": "0.20.2",
22
+ "@voltro/sql-mysql": "0.20.2",
23
23
  "effect": "^3.21.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.20.0",
26
+ "@voltro/testing": "0.20.2",
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.20.0",
14
- "@voltro/database": "0.20.0",
15
- "@voltro/env": "0.20.0",
16
- "@voltro/plugin-multitenancy": "0.20.0",
17
- "@voltro/plugin-storage": "0.20.0",
18
- "@voltro/protocol": "0.20.0",
19
- "@voltro/runtime": "0.20.0",
13
+ "@voltro/cli": "0.20.2",
14
+ "@voltro/database": "0.20.2",
15
+ "@voltro/env": "0.20.2",
16
+ "@voltro/plugin-multitenancy": "0.20.2",
17
+ "@voltro/plugin-storage": "0.20.2",
18
+ "@voltro/protocol": "0.20.2",
19
+ "@voltro/runtime": "0.20.2",
20
20
  "effect": "^3.21.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@voltro/testing": "0.20.0",
23
+ "@voltro/testing": "0.20.2",
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.20.0",
15
- "@voltro/database": "0.20.0",
16
- "@voltro/env": "0.20.0",
17
- "@voltro/plugin-governance": "0.20.0",
18
- "@voltro/plugin-multitenancy": "0.20.0",
19
- "@voltro/protocol": "0.20.0",
20
- "@voltro/runtime": "0.20.0",
14
+ "@voltro/cli": "0.20.2",
15
+ "@voltro/database": "0.20.2",
16
+ "@voltro/env": "0.20.2",
17
+ "@voltro/plugin-governance": "0.20.2",
18
+ "@voltro/plugin-multitenancy": "0.20.2",
19
+ "@voltro/protocol": "0.20.2",
20
+ "@voltro/runtime": "0.20.2",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.20.0",
24
+ "@voltro/testing": "0.20.2",
25
25
  "typescript": "^5.7.0",
26
26
  "vitest": "^3.0.0"
27
27
  }
@@ -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.20.0",
15
- "@voltro/database": "0.20.0",
16
- "@voltro/env": "0.20.0",
17
- "@voltro/plugin-multitenancy": "0.20.0",
18
- "@voltro/protocol": "0.20.0",
19
- "@voltro/runtime": "0.20.0",
20
- "@voltro/workflow": "0.20.0",
14
+ "@voltro/cli": "0.20.2",
15
+ "@voltro/database": "0.20.2",
16
+ "@voltro/env": "0.20.2",
17
+ "@voltro/plugin-multitenancy": "0.20.2",
18
+ "@voltro/protocol": "0.20.2",
19
+ "@voltro/runtime": "0.20.2",
20
+ "@voltro/workflow": "0.20.2",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.20.0",
24
+ "@voltro/testing": "0.20.2",
25
25
  "typescript": "^5.7.0",
26
26
  "vitest": "^3.0.0"
27
27
  }
@@ -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.20.0",
16
- "@voltro/database": "0.20.0",
17
- "@voltro/env": "0.20.0",
18
- "@voltro/plugin-flags": "0.20.0",
19
- "@voltro/plugin-multitenancy": "0.20.0",
20
- "@voltro/protocol": "0.20.0",
21
- "@voltro/runtime": "0.20.0",
22
- "@voltro/sql-postgres": "0.20.0",
15
+ "@voltro/cli": "0.20.2",
16
+ "@voltro/database": "0.20.2",
17
+ "@voltro/env": "0.20.2",
18
+ "@voltro/plugin-flags": "0.20.2",
19
+ "@voltro/plugin-multitenancy": "0.20.2",
20
+ "@voltro/protocol": "0.20.2",
21
+ "@voltro/runtime": "0.20.2",
22
+ "@voltro/sql-postgres": "0.20.2",
23
23
  "effect": "^3.21.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.20.0",
26
+ "@voltro/testing": "0.20.2",
27
27
  "typescript": "^5.7.0",
28
28
  "vitest": "^3.0.0"
29
29
  }