@voltro/cli 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/dist/{apiBuild-370ITNRk.js → apiBuild-BJtiqoOu.js} +2 -2
  3. package/dist/apiBuild-BOczx9-m.js +2 -0
  4. package/dist/bin.js +3 -3
  5. package/dist/{commands-D-teL_TM.js → commands-8A5RU3MW.js} +1785 -1564
  6. package/dist/dbCommand-DNQ6l-su.js +2 -0
  7. package/dist/{dbCommand-Dmi-vHfA.js → dbCommand-OPMtO0sX.js} +225 -211
  8. package/dist/{dev-CKyW_LAv.js → dev-DGjEvvkV.js} +1321 -1300
  9. package/dist/{dev-CPKbckaN.js → dev-HdG7JdSk.js} +1 -1
  10. package/dist/{fileConventions-COg0ZytD.js → fileConventions-Cu0tM-Bl.js} +22 -2
  11. package/dist/{frameworkTableAssembly-DCLhpHsF.js → frameworkTableAssembly-BA2bAmYN.js} +1 -1
  12. package/dist/frameworkTableAssembly-D1zJSgOU.js +2 -0
  13. package/dist/index.js +1 -1
  14. package/dist/{inspectMetrics-CyVMeofr.js → inspectMetrics-CYDSO3za.js} +1 -1
  15. package/dist/{serveCommand-CU3Sw4ZM.js → serveCommand-C3lq3mFd.js} +7 -5
  16. package/dist/serveEntry.js +2 -2
  17. package/dist/{start-D8ATvTzo.js → start-BtgatJN3.js} +1 -1
  18. package/dist/startEntry.js +2 -2
  19. package/package.json +17 -17
  20. package/templates/AGENTS.md +1 -1
  21. package/templates/agent-docs/_index.md +1 -1
  22. package/templates/agent-docs/authentication.md +29 -0
  23. package/templates/agent-docs/cli.md +80 -0
  24. package/templates/agent-docs/data.md +11 -3
  25. package/templates/agent-docs/database/migrations.md +21 -0
  26. package/templates/agent-docs/plugins.md +1 -1
  27. package/templates/agent-docs/schema-driven-ui.md +2 -1
  28. package/templates/agent-docs/whats-new.md +84 -50
  29. package/templates/apps/api-ai/package.json +7 -7
  30. package/templates/apps/api-auth/package.json +8 -8
  31. package/templates/apps/api-backend/package.json +7 -7
  32. package/templates/apps/api-backend-deactivation/package.json +7 -7
  33. package/templates/apps/api-backend-mail/package.json +8 -8
  34. package/templates/apps/api-backend-mariadb/package.json +9 -9
  35. package/templates/apps/api-backend-storage/package.json +8 -8
  36. package/templates/apps/api-data-advanced/package.json +8 -8
  37. package/templates/apps/api-durable/package.json +8 -8
  38. package/templates/apps/api-feature-flags/package.json +9 -9
  39. package/templates/apps/api-governance/package.json +8 -8
  40. package/templates/apps/api-kv/package.json +8 -8
  41. package/templates/apps/api-moderation/package.json +8 -8
  42. package/templates/apps/api-observability/package.json +8 -8
  43. package/templates/apps/api-ratelimit/package.json +8 -8
  44. package/templates/apps/api-rbac/package.json +8 -8
  45. package/templates/apps/api-rest/package.json +7 -7
  46. package/templates/apps/api-saas/package.json +11 -11
  47. package/templates/apps/api-search/package.json +8 -8
  48. package/templates/apps/api-versioning/package.json +8 -8
  49. package/templates/apps/api-webhooks/package.json +9 -9
  50. package/templates/apps/changelog/package.json +6 -6
  51. package/templates/apps/edge-functions/package.json +2 -2
  52. package/templates/apps/frontend-admin/package.json +8 -8
  53. package/templates/apps/frontend-app/package.json +8 -8
  54. package/templates/apps/frontend-blank/package.json +7 -7
  55. package/templates/apps/frontend-contact/package.json +7 -7
  56. package/templates/apps/frontend-dashboard/package.json +7 -7
  57. package/templates/apps/frontend-docs/package.json +7 -7
  58. package/templates/apps/frontend-i18n/package.json +6 -6
  59. package/templates/apps/frontend-landing/package.json +7 -7
  60. package/templates/apps/frontend-spa/package.json +7 -7
  61. package/templates/apps/frontend-ssr/package.json +7 -7
  62. package/templates/apps/frontend-ssr-api/package.json +8 -8
  63. package/templates/apps/frontend-static-blog/package.json +6 -6
  64. package/dist/apiBuild-DatWP5hh.js +0 -2
  65. package/dist/dbCommand-B3CeMU3u.js +0 -2
  66. package/dist/frameworkTableAssembly-DYzb2elg.js +0 -2
@@ -1,4 +1,4 @@
1
- # What's new in 0.13.0
1
+ # What's new in 0.15.0
2
2
 
3
3
  Read this FIRST when a task touches an area you have not worked in recently.
4
4
  It is the cheapest way to notice that the framework grew the thing you were
@@ -9,89 +9,123 @@ BREAKING entries name a codemod; run `voltro update` to apply it.
9
9
 
10
10
  ### ⚠ BREAKING
11
11
 
12
- - **@voltro/protocol, @voltro/plugin-scim, @voltro/plugin-prometheus** SCIM was served UNAUTHENTICATED whenever its token was an empty string.
12
+ - **@voltro/cli** **A rename now carries the ALIASED importers, not only the relative ones.**
13
13
 
14
- `checkBearer(headers, expected)` returned `true` when `expected` was unset or empty, documented as "no token configured = open; the caller decided not to gate this surface". Its one production caller had decided the opposite: `scimPlugin` declares `token: string`, and `scimPlugin({ token: process.env.SCIM_TOKEN ?? '' })` the shape anyone writes turned the gate off silently. The result was SCIM 2.0 Users and Groups readable with no credentials: a full directory dump plus the provisioning surface that can deactivate accounts. Likeliest exactly where it hurts, too: an env var set in production and missing in a preview environment.
14
+ An app's web build stopped compiling after `voltro update`: 84 relative imports were rewritten correctly, 163 aliased ones across 88 files were not, and `tsc` reported 249 errors on names that no longer existed. Nothing in the codemod's output hinted that a whole class of import had been skipped.
15
15
 
16
- `checkBearer` is now fail-closed by default, with the permissive behaviour available as an explicit `{ openWhenUnset: true }` — a two-argument helper cannot know its caller's intent, so it must not assume the permissive one. `@voltro/plugin-prometheus` passes it (its token is documented as optional), and `scimPlugin` now throws at construction — i.e. at boot — rather than answering the first anonymous request.
17
- - **@voltro/database, @voltro/cli** — `voltro db apply` and boot auto-migrate could report success while applying nothing, and then record a fingerprint that made every later boot short-circuit on "schema up to date".
16
+ Two independent halves, and each alone leaves the imports stale:
18
17
 
19
- Reported from a live pod: `applied 31 op(s)` on every boot for two releases, with none of the 31 present in the database. Nothing was wrong with the transport, the lock or the transaction the applier emitted statements that postgres accepted and that changed nothing. Two independent causes:
18
+ - The codemod's ts-morph project was built with **no `baseUrl` and no `paths`**, so `@/components/link` resolved to nothing. It now gets the compiler's own shape — the raw `paths`, not the pre-resolved Vite table, because a `paths` target is relative to `baseUrl` by definition and an absolute one does not resolve. - `SourceFile.move()` rewrites relative specifiers and **nothing else**, which is correct on its own terms: ts-morph cannot know whether the alias mapping or the file is meant to change. So the codemods now rewrite the aliased ones themselves, narrowly only specifiers that RESOLVED to the moved file, and only the trailing stem, which needs no alias table and so cannot disagree with one. The run reports how many it rewrote.
20
19
 
21
- - A `ColumnSnapshot` carried no `vector` dimension / `array` element / `enum` name, so the applier's type renderers collapsed all three to `text`. A declared `vector(1536)` over a live `text` column planned an `alter-column-type` that emitted `ALTER COLUMN TYPE text`. Valid, applied, no-op, re-planned forever. (Also meant an `add-column` for a vector, array, enum or PostGIS column created a plain `text` column.) - The default-clause renderers excluded ARRAYS, returning `null`, and the call site turned that into `SET DEFAULT NULL`. A declared `.default([])` on a `json()` column therefore never landed — thirty columns were stuck this way in the reporting schema.
20
+ **A file an exact `paths` entry names is left alone and reported.** Renaming `link.tsx` while `"@/link": ["src/components/link.tsx"]` points at it leaves the mapping resolving to nothing and the same path is usually repeated in a vite/vitest alias table no codemod owns. One app hit this and then saw a taxonomy violation reported on a file the codemod had itself created.
22
21
 
23
- Fixed: the snapshot carries the type parameters and the renderers delegate to `migrate.ts`'s canonical `sqlType`, so the applier and the CREATE-TABLE emitter cannot disagree; array defaults render (a real `text[]` literal on a native `array()` column, a jsonb literal otherwise); and a default the renderer cannot express now FAILS instead of degrading to `DEFAULT NULL`.
22
+ **`page/unsuffixed-in-pages` is now an error, not a warning.** The comment justifying the warning contradicted the scanner it described: that bucket is already narrowed to a default export nothing imports, which is what an unmigrated page looks like and what a co-located component never does. The failure it names is invisible everywhere else — an unmigrated route simply 404s, with a clean `tsc` and a green suite. One app finished a migration with 51 of them. `codemod: none`: the rename it asks for already ships as `0.14.0/03_pages-suffix`, and nothing about a user's SOURCE changes here what changes is that `voltro check` now fails on a route that does not route.
23
+ - **@voltro/protocol, @voltro/client, @voltro/web** — **`errorTag` moves from `@voltro/client` to `@voltro/protocol`.**
24
24
 
25
- And the structural guard, which is the part that matters: **`applyPlan` re-plans against the live schema before it records a fingerprint, and refuses to record one if any operation remains.** DDL that changes nothing succeeds exactly as quietly as DDL that works, so the only evidence a plan applied is that the same planner has nothing left to do. `ApplyPlanCtx` gains a required `replan`; `AppliedMigration` gains `appliedOps` (what EXECUTED, not `plan.operations.length`), and the boot log quotes that.
26
- - **@voltro/plugin-storage** — `storage.share`, `storage.revoke` and `storage.listGrants` performed no authorization at all.
25
+ It reads the `_tag` that `toRpc` writes, so it now lives beside `toRpc` one file owning both ends of that contract. Where it used to live had a cost invisible from inside the framework: an app's shared error handler sat in a package that pulled only `@voltro/i18n`, and reading a tag would have meant depending on the entire client package for seven lines. They declined, and kept parsing message strings with a regex the exact outcome the helper exists to prevent.
27
26
 
28
- Each took an object id straight off the wire and passed it to a service method that (correctly, for a trusted server-side API) checks nothing, with nothing in between. Any authenticated caller could grant themselves read or write on any object in the installation, revoke anyone else's grants, and enumerate who an object is shared with.
27
+ `@voltro/web` re-exports the client surface, so it loses the symbol too the same codemod covers an app that imported it from there.
29
28
 
30
- All three now require that the caller owns the object, or carries `admin:full`. A missing object and an unowned object report the same 403 a 404 would let an unauthorized caller probe which ids exist. `GrantStore` gains `getById`, which `revoke` needs to resolve a grant id back to its object.
29
+ Not re-exported from `@voltro/client`: two import paths for one helper is how the next reader learns the wrong one. The transform codemod repoints the import, preserving an alias (`errorTag as tagOf`) and the type-only form, and merges into an existing `@voltro/protocol` import rather than adding a second.
30
+
31
+ While moving it, its doc comment gained the thing that matters at the call site and was only implied before: **`instanceof` does not hold on the client.** What arrives there was decoded from JSON and never constructed, so match on the tag, not on the class. One team read the old wording as a promise that `instanceof` works and was right to say so.
31
32
 
32
33
  ### Added
33
34
 
34
- - **@voltro/runtime, @voltro/database** — API keys carry app-owned `metadata` the second ownership axis.
35
+ - **@voltro/protocol, @voltro/cli** — **An auth strategy reaches the app's DataStore, on `input.store`.**
36
+
37
+ ```ts
38
+ const sessionStrategy: AuthStrategy = {
39
+ id: 'db-session',
40
+ resolve: async ({ headers, store }) => {
41
+ if (store === undefined) return { kind: 'skip' } // still booting
42
+ const [row] = await store.query(sessions.byToken(headers.authorization))
43
+ return row ? { kind: 'matched', subject: toSubject(row) } : { kind: 'skip' }
44
+ },
45
+ }
46
+ ```
35
47
 
36
- `tenantId` and `onBehalfOf` are the two relationships the framework models. Plenty of apps have a third that actually authorizes the key: a team, a project, an environment. `ApiKeyRecord` in `@voltro/protocol` has carried a `metadata` slot all along its doc comment even names `teamId` as the example but the SERVICE had nowhere to store it and nowhere to return it. So an app with a team axis could authenticate through the built-in strategy and still not authorize, and `apiKeys: true` was unusable for it. Reported as the one thing that stopped an otherwise complete adoption; their alternatives were a second table joined on the hot auth path, or smuggling `team:<id>` into `scopes`, where `hasScope` would then see a scope that is not a scope.
48
+ Without it, a DB-backed strategy a session row, an API-key record, a PAT table had to open a SECOND connection path beside the framework's, to the same database the request store opens a moment later. One adopter's `auth/db.ts` is 105 lines of exactly that: a second `ManagedRuntime` plus a `MysqlClient`, load-bearing for their session lookup and their ApiKeyStore. Every DB-backed OIDC / SAML / PAT integration rebuilds it, which is what made this a framework gap rather than an app's problem.
37
49
 
38
- `IssueInput`, `ApiKeyRow` and `ResolvedApiKey` now carry it, stored as JSON on `_voltro_api_keys`, and it survives `rotate` a rotated key is the same credential with a new secret, so dropping it would silently de-authorize every rotated key.
50
+ It is the **same value** `auth.resolveScopes` already receives, through the **same lazy getter** — one ref, two consumers, rather than each caller reaching for the store its own way. That is deliberate: `voltro dev` builds the store AFTER the auth chain and `voltro serve` builds it BEFORE, so a value captured at config time would be `undefined` forever in dev and correct in production. The getter is read ONCE per request, not once per strategy.
39
51
 
40
- It is app data, never identity. The strategy merges it UNDER the framework's own claims: `provider` and the acting `userId` are written afterwards from `onBehalfOf` and always win, including when the answer is "none". A bag that could set `userId` would let whoever minted the key choose who the request is. Pinned end-to-end, not just at the protocol layer.
52
+ `store` is `undefined` only while the store is still being built, and on an app with no store a strategy should `skip` rather than throw. It is the BOOT store, not a request-scoped one: strategies resolve before a request store exists.
41
53
 
42
- `PublicApiKey` also gains `createdBy` and `onBehalfOf`, so `service.list` can answer the two questions an admin actually asks about a shared credential. Neither is a secret they are the accountability record, and omitting them hid them from the person responsible for the key.
43
- - **@voltro/protocol, @voltro/cli** — A boot warning when two auth strategies claim the same bearer-token prefix.
54
+ **Not narrowed to a read-only surface**, and the reason is worth stating: the narrower type would be the better guarantee, `DataStore` is the driver SPI, and giving strategies a different type from the one `resolveScopes` gets would put two views of one object in the same file. A strategy that writes during subject resolution is a design mistake; the type system is not going to catch it for you. Read users / sessions / keys, do not run domain writes.
44
55
 
45
- The chain is first-match-wins, so a duplicate claim is not a harmless redundancy: whichever strategy runs first decides the Subject. An app that already has its own `sk_` keys and then sets `apiKeys: true` gets the framework strategy appended on the same prefix — resolving without the app's own team binding — and *which strategy answered* decides whether authorization works. Reported by an app that had to pin a test asserting it never enables the flag.
56
+ ### Fixed
46
57
 
47
- `AuthStrategy` gains an optional `claimsBearerPrefix`, set by `apiKeyStrategy` from its `prefix` option. Making the claim declarative is what makes the collision detectable at all the same "only what is declared can be checked" argument the scope rules run on. Checked in `buildResolveSubject`, which both `voltro dev` and `voltro serve` call, so the two boot paths cannot drift.
58
+ - **@voltro/cli** — `voltro dev` terminates when its dev server does, instead of living forever.
48
59
 
49
- A warning rather than a refusal: two strategies on one prefix can be deliberate (a migration window where old and new keys share a shape). What must not happen is that it goes unmentioned.
50
- - **@voltro/protocol, @voltro/cli** — `auth.resolveScopes` — add scopes to an authenticated Subject from your own data, so ROLE-based authorization becomes declarable.
60
+ The supervisor never watched its child die. `runChild` was an `Effect.acquireUseRelease` whose `use` was `Effect.never`, so `proc.on('exit')` existed ONLY in the release path — and that path runs when the fiber is interrupted (a restart, a signal), never when the child exits by itself. Two more places assumed the same thing: `runSupervisor` was typed `Effect<never>`, and `dev.ts` returned `new Promise(() => {})` after starting it.
51
61
 
52
- An app whose authorization is a database role (`requireCallerAdmin(ctx)` reading an `employees.role` column) is invisible to every static check the framework has: `voltro check`'s `rbac/unguarded-mutation` reports its writes as unguarded, and it is right to nothing about the decision is declared. But the declarative alternative was unusable for exactly those apps: their subjects come from an external IdP's JWTs and carry no scopes, so `requireScope('employee:admin')` would lock out every real user. One app measured 1566 findings it had no way to act on.
62
+ So a boot that aborted an unreachable database, a refused migration, a failed env gate left the child dead and the supervisor waiting for a file change that nobody was there to make. Measured, not inferred: one developer machine carried 15 such `voltro dev` process pairs, `ppid=1`, the oldest 7 days old, every one a boot that had failed against a remote database. They hold a watcher and a terminal-less process each; in CI the same shape keeps a runner busy after the job "finished".
53
63
 
54
- Lifting the role into `subject.scopes` makes the SAME authorization declarable, visible in the manifest and checkable in CI. Deliberately narrow: the hook returns SCOPES, never a Subjectit cannot change `id` or `tenantId` (identity belongs to the auth strategy), and the result is unioned with the strategy's own scopes, so it can grant but never revoke. It runs per matched request, so cache the lookup yourself; the framework does not, because only the app knows how fast a role change must take effect. Wired identically in `voltro dev` and `voltro serve`.
55
- - **@voltro/cli** — `voltro doctor` reports packages resolved at more than one version.
64
+ `use` now awaits the child (`awaitChildExit`) and the supervisor races that against the watch loop, so whichever happens first decides. A restart still does NOT end it — `stopChild` interrupts the fiber, so the deferred is never completed on that path. A child killed by a signal reports `code: null`, which is reported as a failure rather than a clean 0.
56
65
 
57
- A consumer reported type errors inside the GENERATED `rpcGroup.generated.ts` — `Property '[TypeId]' is missing`, `typeof Never is not assignable to All`, an `Rpc<…, Stream<…>, …>` refused where `Any` was expected — and reasonably concluded the framework emits bad types, because the errors land in a file they cannot edit and did not write. That is the signature of two copies of `effect` in one install: Effect's types are nominal, so a Schema built by one copy is not the type the other expects.
66
+ What a self-exit MEANS then depends on whether anyone is watching, because the two failure modes pull in opposite directions:
58
67
 
59
- It deserves its own check because the RUNTIME usually stays green two instances only diverge where identity matters — so an app boots, serves and passes its tests while `tsc` is red, which sends people looking at the compiler instead of the dependency tree. The report names the versions, the paths, and the errors it explains. Only identity-sensitive packages count (`effect`, `@effect/*`, `@voltro/*`, react/react-dom); a duplicated string utility is wasteful, not a bug class.
60
- - **@voltro/cli** — `voltro doctor` flags an executor that never names its own descriptor.
68
+ - **Interactive** (stdout is a TTY) a crashed boot is something you are about to fix, so the supervisor says so and keeps watching. The next save restarts it, which is what every other dev server does; stopping would throw away the watcher mid-edit and make you retype the command. - **Non-interactive** nobody is going to fix anything. `voltro dev` exits with the child's code, so a failed boot is a failed command. This is the case that produced the invisible processes, and the one CI actually waits on.
61
69
 
62
- Descriptor/executor pairing is by FILENAME, which is right and it means a `*.server.ts` can be a complete, correct executor with no reference at all to the contract it implements. Those are exactly the files where a hand-written input drifts from the wire.
70
+ A CLEAN exit always stops, watched or not. `VOLTRO_DEV_KEEP_ALIVE=1|0` forces the answer for what the TTY check cannot see — a CI runner with a TTY allocated, or a wrapper that pipes output while a human still watches it and cannot keep a clean exit alive, which would turn a deliberate shutdown into a hang.
63
71
 
64
- Reported after a 426-executor migration to `ExecutorInput<typeof descriptor>`: three files were skipped by the app's own codemod for a reason no reviewer would guess they never imported their descriptor, so there was no `typeof` to point at. In the same codebase, six executors had written `boardPurpose: string` where their descriptor declared `Schema.Literal(...)`, discarding the contract at the executor boundary. Only imports of a SIBLING module clear the finding: an executor importing nothing but `@voltro/*` and `node:*` has still not named its contract.
65
- - **@voltro/database** — `updateManyRow(store, table, patch, { where })` the last untyped write is now typed against its table.
72
+ Pinned against REAL child processes, because the defect was an Effect that never settleda stubbed `once('exit')` that resolves is exactly what would have passed while the bug shipped.
73
+ - **@voltro/database** — **A user-facing message that names an API must have one now checked in CI.**
66
74
 
67
- `insertRow` and `upsertRow` already were; `ctx.store.updateMany(table, row, { where })` still took a string table name and an untyped row literal. Worth closing because the typed versions were measured: migrating 29 `store.upsert` call sites to `upsertRow` produced 15 `tsc` errors across 8 distinct defects that no test had caught — including seven per-user mutations with no authentication check at all (they wrote `ctx.request.subject.id`, typed `string | null`, into a NOT NULL column, so an anonymous caller reached the database and got a raw statement failure instead of a typed refusal).
75
+ The sibling of the claimed-wiring check. That one asserts a doc comment's claimed caller exists; this one asserts a message's claimed API exists. Same failure shape, worse audience: a doc comment is read by somebody browsing, a refusal by somebody already blocked and looking for the sanctioned way out.
68
76
 
69
- ### Fixed
77
+ It exists for a reported bug that nothing could have caught. The drop-table refusal offered, as its FIRST option, *"chain `.dropped()` on it"* — tables have no such marker, only columns do. Doc SAMPLES are typechecked; message strings are not, and cannot be. Three of one release's reported defects lived in that blind spot.
70
78
 
71
- - **@voltro/runtime** A `cache:` declared on a query whose handler returns a COMPUTED value was silently ignored; it now says so.
79
+ Three rules, each with an unambiguous answer, because a noisy gate is skipped and then costs more than it saves:
72
80
 
73
- The snapshot cache wraps the store read, and a computed query has none its handler has already run by the time the binding is built. Caching one would mean wrapping the handler invocation, which is a different feature. Until that exists, the honest failure is a loud one: silently ignoring the config is how an author ends up believing a hot query is cached while every subscriber re-runs it. The data stays correct, so nothing else would ever tell them. Warned once per query name, not per subscribe.
74
- - **@voltro/cli** — The minted `.env.local` is handed to the workspace's owner, and an unreadable env file explains itself.
81
+ - a `VOLTRO_*` variable a message tells you to **set** must be read somewhere, - a `` `.method()` `` a message tells you to **chain** must be a callable MEMBER of a published type, - a `--flag` in a `voltro …` instruction must be parsed.
75
82
 
76
- A dev container running as root with the host workspace bind-mounted wrote `apps/api/.env.local` as `root:root 0600` INTO THE SHARED WORKSPACE. On the host, everything that loads env then died with EACCES vitest, `voltro doctor`, the editor and the developer could not even read the file, while the next container boot recreated it. Container-with-bind-mount is the ordinary dev shape, not an edge case.
83
+ The member rule is the one that took two attempts. The first version asked "does this name exist in the public surface" and the motivating bug **passed it**: `dropped` is exported, as a free `dropped()` you write as a column's value. A dotted claim is a claim about something chainable, so a free function and a `readonly dropped?: boolean` data property are both correctly rejected now.
77
84
 
78
- `0600` stays (the file holds a real signing key), because loosening it to `0644` would make that key readable by every account on the machine for the far more common single-user case. Ownership was the wrong variable, so that is the one corrected: the mint chowns the file to whoever owns the directory, which root can do exactly the case that needs it — and reports loudly when it cannot. A plain EACCES while loading an env file now names the owning uid, the mode and the current uid, because that pair IS the diagnosis and none of it appears in node's message.
79
- - **@voltro/cli** — Framework-generated output is handed to the workspace's owner, not left owned by whoever the process happens to be.
85
+ **It immediately found a second instance nobody had reported** the drop-COLUMN refusal also said "chain `.dropped()`", one level down from the reported one, and the real spelling is `<column>: dropped()` as the field's value. Close enough to guess from, which is why it survived.
80
86
 
81
- The previous release fixed this for the minted `.env.local`. The report that followed showed the scope was wrong: it is EVERY directory the framework generates. A dev pod running as root with the host monorepo bind-mounted leaves `.framework/` and `app.graph.observed.*` as `root:root` inside the developer's own tree, and on the host:
87
+ Ships with a `--selftest` that runs first in CI, for the reason the changelog gate has one: a check that has quietly stopped detecting anything still prints green, and green is read as evidence.
88
+ - **@voltro/database** — **Two migration refusals sent people the wrong way** — the worst place for a bad hint, because whoever reads one is already blocked and looking for the sanctioned way out.
82
89
 
83
- ```
84
- voltro build . → EACCES: permission denied, open '…/apps/display/.framework/index.html'
85
- ```
90
+ **The drop-table refusal recommended an API that does not exist.** Its first option was *"add it to your declared set + chain `.dropped()` on it"*. There is no table-level `dropped()` — only the column marker. The recommendation was also the conceptually RIGHT one, which is what made it expensive: the two options that do work are both worse, so a reader picks the one they cannot follow.
91
+
92
+ There is now a real per-table answer: **`VOLTRO_DESTRUCTIVE_OK` accepts a table list**, not just `1`. `VOLTRO_DESTRUCTIVE_OK=old_things` acknowledges the data loss for that table and leaves every other lossy op in the plan blocked. `1` still means all of them — which is rarely what somebody means, and was previously the only way to say anything. A user with one intended drop and three other lossy ops had to acknowledge all four or hand-write a `DROP TABLE` migration, the path 0.14.0's own upgrade note warns against.
93
+
94
+ The message also states why there is deliberately no table marker: a dropped COLUMN leaves a slot worth documenting in the declaration; a dropped TABLE leaves nothing, so the marker would be a dead entry you must remember to delete.
95
+
96
+ **The drop-column refusal never mentioned `renamedFrom`.** It offered "chain `.dropped()`" or "restore the field" — and followed literally on a rename, the first costs exactly the data the user was trying to keep. When the plan drops AND adds columns on the same table, the message now leads with *"did you rename one?"* and names both sides. The evidence was in the plan the whole time.
97
+
98
+ Finding that required fixing a second thing: the footer read only the BLOCKED operations, and an `add-column` is `safe`. The counterpart of a rename was never in the list it was looking at.
99
+ - **@voltro/cli** — **0.14.0's taxonomy codemod renamed two kinds of file it should not have, and a repo that already upgraded carries the damage with a green build.** Both were found by adopters running it on real projects; both are silent — the rename succeeds, the imports are rewritten, nothing throws.
100
+
101
+ **A framework primitive was treated as an undeclared file.** `health.route.tsx` → `health.route.component.tsx`, five times in one app. The codemod kept its OWN list of "suffixes that already carry a contract" instead of reading `fileConventions.ts`, and `.route.` was not on it — the exact drift that module exists to prevent, reproduced inside a file that imports from it. The list is gone; the registry answers now, and it gained `ROUTE_PATTERN` plus a `carriesFrameworkConvention()` every consumer shares.
102
+
103
+ The root cause underneath was worse than a missing entry: `export default defineRestRoute({...})` resolved to the placeholder name `Default`, which starts with a capital, and was counted as a COMPONENT on that basis. A default export is now only evidence of a component when the exported thing is callable — so a convention nobody has registered yet is safe too.
104
+
105
+ **A file with no exports was called a type file.** `test-setup.ts` → `test-setup.types.ts`, while `vitest.config.ts` still named `./test-setup.ts` as a **string**. Not an import, so nothing rewrote it and nothing failed: that suite would have run without its setup and stayed green. Five more went the same way — a registry module, two migration runners, a `.register.ts`, and a code generator whose `export` tokens live inside template strings.
106
+
107
+ `*.types.ts` promises "zero runtime exports", and that promise only means something for a file that exports TYPES. Zero of everything promises nothing, and renames a module whose whole purpose is being imported for effect — where the filename is often the only reference there is. It now requires at least one exported type.
108
+
109
+ **The shipped codemod undoes both**, and can only reach files whose content proves the suffix was wrong: a `.component.` on a name that already carries a framework convention, and a `.types.` on a file that exports nothing at all. It also prints the one thing it cannot fix — references by PATH rather than by import (a vitest `setupFiles`, a tsconfig `include`, a Docker `COPY`) were strings on the way out and are strings on the way back.
110
+ - **@voltro/cli** — Three ways `voltro update` failed on a real adopter's host, none of which we could have found ourselves — each needs a machine we do not have.
111
+
112
+ **The install could not run here, and the refusal left the tree half-upgraded.** Their install runs in a container against its own store. `voltro update` ran the package manager on the host anyway; pnpm refused (it wanted to remove `node_modules` and had no TTY to ask) and exited — after the version bumps were already written and before any codemod ran. That is the state this command's own documentation calls the worst one to be in, and it was reachable by design.
113
+
114
+ `--no-install` now writes the bump and stops, saying plainly that the tree is half-upgraded and naming both remaining steps. The install-failed message points at it too. Note what this is not: a compatibility flag. It is a mode for a host where the install is somebody else's job, and it ends by telling you the job is not done.
115
+
116
+ **The codemod scan exhausted a 12 GB heap, and said nothing about why.** The crash was a bare V8 out-of-memory stack. The scan pruned the directories WE know about — `node_modules`, `dist`, `.turbo` — which cannot cover a project's own heavy ignored trees (a build cache, a data dump, a virtualenv).
117
+
118
+ Inside a git repository the scan now asks git: `git ls-files --cached --others --exclude-standard` is exactly "files this project considers its own", and a codemod rewrites source — source that git ignores is not source we may rewrite. It also removes the traversal, so there is nothing left to exhaust memory on. Outside a repo the walk remains, now with a ceiling that REPORTS which directory to exclude instead of dying namelessly.
119
+
120
+ **"not a Voltro app" was the wrong conclusion.** Said of a directory containing an `app.config.ts`, it sends the reader looking for the wrong problem. Three web apps in a workspace inherited from another tool had their `@voltro/*` dependencies in an ancestor `package.json` — the apps ARE Voltro apps; only the declaration lives elsewhere. With an `app.config.ts` present the message now says that, and names the two ways forward.
86
121
 
87
- That is the harder failure. `.env.local` broke env loading; this breaks the production build of every web app outright, with no workaround short of chown-ing by hand after each pod boot. One team could only verify their frontends through test suites and live requests against the running pods.
122
+ **A monorepo may keep ONE root `package.json`** with its apps carrying only an `app.config.ts`. Running `voltro update` inside such an app used to say "no package.json at <dir>" true, and useless. It now recognises the layout, says it is supported, and prints the command with the root already filled in.
123
+ - **@voltro/cli** — **`voltro update --only <id>`**, and a summary that stops contradicting itself.
88
124
 
89
- `voltro dev` and `voltro build` now hand their generated output `.framework`, `.env.local`, every `*.generated.*` to the uid that owns the app root, and say so loudly when they cannot. A no-op on every ordinary run and in any container started with `--user <uid>:<gid>`: when the process already owns the root it returns without touching the tree. Only generated state is claimed; the framework never chowns a file a human wrote.
90
- - **@voltro/cli** — The observed app-graph no longer restarts the dev server.
125
+ `--only` runs just the named codemods (repeatable or comma-separated; ids are what `--dry-run` prints). The ask behind it: two of three codemods were load-bearing for one app without them, 52 type errors and 51 routes that 404 while the third was elective, and the repairing tool refuses on a dirty tree, so there was no way to take the necessary half first.
91
126
 
92
- `app.graph.observed.json` was written into the watched app root every 10 seconds, and the supervisor's watcher fired on each write. A downstream pod measured two restarts before every boot over 2000 log lines the rule, not an outlierand paid a ~46 s boot three times per save.
127
+ It is deliberately NOT a `--required` flag over a REQUIRED/OPTIONAL axis on each codemod. "Required" would have to mean *this app does not run without it*, and that is a property of the app: `03_pages-suffix` is unavoidable for a project with pages and irrelevant to an api-only one. Marking it on the codemod would encode a guess as a contract. An unknown id is an error listing the ids that ARE available"it did nothing" and "you typed it wrong" otherwise look identical.
93
128
 
94
- The watcher excludes `<name>.generated.<ext>`, a substring rule chosen precisely because a per-extension whitelist had already let a generated file slip twice. This file slipped it a third time by not carrying the segment at all. It is now `app.graph.observed.generated.json`, which matches the convention instead of adding a fourth special case to a list that has drifted three times; a stale un-suffixed file from an older dev server is removed on boot so it cannot keep triggering restarts.
95
- - **@voltro/cli** — Four tooling fixes, all from downstream reports:
129
+ **The summary counted only edited files, and renames vanished from it.** The before-snapshot is keyed by PATH, so a moved file has no entry under its new one: a pass that renamed 238 files and edited 9 importers reported `(9 files)`, understating the change by a factor of 26 in the line a user plans around. Moves are now paired by content and reported separately `(238 renamed, 9 edited)`.
96
130
 
97
- - **`voltro check --offline` crashed on any app that declares a workflow.** It built workflow entries as `{ name }` behind an `as never` while `InspectWorkflowEntry` is keyed by `tag`, so the manifest's sort read `undefined` and threw surfacing as "could not assemble the graph from source" rather than the type error underneath. The cast is what let the two shapes disagree. - **`voltro check --offline` reported plugin tables as `dangling-source`.** It collected only the app's own `*.entity.ts` tables, so a query reading `_voltro_storage_refs` was an `error` — which sets the exit code, failing the CI gate the offline mode exists for. It now uses the same `assembleFrameworkTables` the migrator does. - **`voltro test` now derives `resolve.alias` from the app's tsconfig `paths`.** An app mapping `@/* → ./src/*` could not test any module importing through it (`Cannot find package '@/locales/en'`), and the workaround was a local `vitest.config.ts` restating what tsconfig already said. - **The `raw-fetch` doctor rule follows the import graph.** Keyed on filename conventions it caught 9 of 39 outbound calls on the reporting app; the other 30 were in `lib/*.ts` helpers only server code imports. A file reachable from a server-convention file and from nothing else is server code; one a page also imports is not, and stays unflagged.
131
+ **A dry run no longer speaks in the past tense.** It printed `✓ <id><title>`, the same line a real run prints. Now `·` and `[would apply]`.
@@ -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.14.0",
15
- "@voltro/cli": "0.14.0",
16
- "@voltro/database": "0.14.0",
17
- "@voltro/env": "0.14.0",
18
- "@voltro/protocol": "0.14.0",
19
- "@voltro/runtime": "0.14.0",
14
+ "@voltro/ai": "0.16.0",
15
+ "@voltro/cli": "0.16.0",
16
+ "@voltro/database": "0.16.0",
17
+ "@voltro/env": "0.16.0",
18
+ "@voltro/protocol": "0.16.0",
19
+ "@voltro/runtime": "0.16.0",
20
20
  "effect": "^3.21.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@voltro/testing": "0.14.0",
23
+ "@voltro/testing": "0.16.0",
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.14.0",
16
- "@voltro/database": "0.14.0",
17
- "@voltro/env": "0.14.0",
18
- "@voltro/plugin-auth": "0.14.0",
19
- "@voltro/protocol": "0.14.0",
20
- "@voltro/runtime": "0.14.0",
21
- "@voltro/sql-postgres": "0.14.0",
15
+ "@voltro/cli": "0.16.0",
16
+ "@voltro/database": "0.16.0",
17
+ "@voltro/env": "0.16.0",
18
+ "@voltro/plugin-auth": "0.16.0",
19
+ "@voltro/protocol": "0.16.0",
20
+ "@voltro/runtime": "0.16.0",
21
+ "@voltro/sql-postgres": "0.16.0",
22
22
  "effect": "^3.21.2"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.14.0",
25
+ "@voltro/testing": "0.16.0",
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.14.0",
16
- "@voltro/database": "0.14.0",
17
- "@voltro/env": "0.14.0",
18
- "@voltro/plugin-multitenancy": "0.14.0",
19
- "@voltro/protocol": "0.14.0",
20
- "@voltro/runtime": "0.14.0",
15
+ "@voltro/cli": "0.16.0",
16
+ "@voltro/database": "0.16.0",
17
+ "@voltro/env": "0.16.0",
18
+ "@voltro/plugin-multitenancy": "0.16.0",
19
+ "@voltro/protocol": "0.16.0",
20
+ "@voltro/runtime": "0.16.0",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.14.0",
24
+ "@voltro/testing": "0.16.0",
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.14.0",
16
- "@voltro/database": "0.14.0",
17
- "@voltro/env": "0.14.0",
18
- "@voltro/plugin-deactivation": "0.14.0",
19
- "@voltro/protocol": "0.14.0",
20
- "@voltro/runtime": "0.14.0",
15
+ "@voltro/cli": "0.16.0",
16
+ "@voltro/database": "0.16.0",
17
+ "@voltro/env": "0.16.0",
18
+ "@voltro/plugin-deactivation": "0.16.0",
19
+ "@voltro/protocol": "0.16.0",
20
+ "@voltro/runtime": "0.16.0",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.14.0",
24
+ "@voltro/testing": "0.16.0",
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.14.0",
16
- "@voltro/database": "0.14.0",
17
- "@voltro/env": "0.14.0",
18
- "@voltro/plugin-mail": "0.14.0",
19
- "@voltro/plugin-multitenancy": "0.14.0",
20
- "@voltro/protocol": "0.14.0",
21
- "@voltro/runtime": "0.14.0",
15
+ "@voltro/cli": "0.16.0",
16
+ "@voltro/database": "0.16.0",
17
+ "@voltro/env": "0.16.0",
18
+ "@voltro/plugin-mail": "0.16.0",
19
+ "@voltro/plugin-multitenancy": "0.16.0",
20
+ "@voltro/protocol": "0.16.0",
21
+ "@voltro/runtime": "0.16.0",
22
22
  "effect": "^3.21.2",
23
23
  "react": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.14.0",
26
+ "@voltro/testing": "0.16.0",
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.14.0",
16
- "@voltro/database": "0.14.0",
17
- "@voltro/env": "0.14.0",
18
- "@voltro/plugin-multitenancy": "0.14.0",
19
- "@voltro/plugin-storage": "0.14.0",
20
- "@voltro/protocol": "0.14.0",
21
- "@voltro/runtime": "0.14.0",
22
- "@voltro/sql-mysql": "0.14.0",
15
+ "@voltro/cli": "0.16.0",
16
+ "@voltro/database": "0.16.0",
17
+ "@voltro/env": "0.16.0",
18
+ "@voltro/plugin-multitenancy": "0.16.0",
19
+ "@voltro/plugin-storage": "0.16.0",
20
+ "@voltro/protocol": "0.16.0",
21
+ "@voltro/runtime": "0.16.0",
22
+ "@voltro/sql-mysql": "0.16.0",
23
23
  "effect": "^3.21.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.14.0",
26
+ "@voltro/testing": "0.16.0",
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.14.0",
14
- "@voltro/database": "0.14.0",
15
- "@voltro/env": "0.14.0",
16
- "@voltro/plugin-multitenancy": "0.14.0",
17
- "@voltro/plugin-storage": "0.14.0",
18
- "@voltro/protocol": "0.14.0",
19
- "@voltro/runtime": "0.14.0",
13
+ "@voltro/cli": "0.16.0",
14
+ "@voltro/database": "0.16.0",
15
+ "@voltro/env": "0.16.0",
16
+ "@voltro/plugin-multitenancy": "0.16.0",
17
+ "@voltro/plugin-storage": "0.16.0",
18
+ "@voltro/protocol": "0.16.0",
19
+ "@voltro/runtime": "0.16.0",
20
20
  "effect": "^3.21.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@voltro/testing": "0.14.0",
23
+ "@voltro/testing": "0.16.0",
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.14.0",
15
- "@voltro/database": "0.14.0",
16
- "@voltro/env": "0.14.0",
17
- "@voltro/plugin-governance": "0.14.0",
18
- "@voltro/plugin-multitenancy": "0.14.0",
19
- "@voltro/protocol": "0.14.0",
20
- "@voltro/runtime": "0.14.0",
14
+ "@voltro/cli": "0.16.0",
15
+ "@voltro/database": "0.16.0",
16
+ "@voltro/env": "0.16.0",
17
+ "@voltro/plugin-governance": "0.16.0",
18
+ "@voltro/plugin-multitenancy": "0.16.0",
19
+ "@voltro/protocol": "0.16.0",
20
+ "@voltro/runtime": "0.16.0",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.14.0",
24
+ "@voltro/testing": "0.16.0",
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.14.0",
15
- "@voltro/database": "0.14.0",
16
- "@voltro/env": "0.14.0",
17
- "@voltro/plugin-multitenancy": "0.14.0",
18
- "@voltro/protocol": "0.14.0",
19
- "@voltro/runtime": "0.14.0",
20
- "@voltro/workflow": "0.14.0",
14
+ "@voltro/cli": "0.16.0",
15
+ "@voltro/database": "0.16.0",
16
+ "@voltro/env": "0.16.0",
17
+ "@voltro/plugin-multitenancy": "0.16.0",
18
+ "@voltro/protocol": "0.16.0",
19
+ "@voltro/runtime": "0.16.0",
20
+ "@voltro/workflow": "0.16.0",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.14.0",
24
+ "@voltro/testing": "0.16.0",
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.14.0",
16
- "@voltro/database": "0.14.0",
17
- "@voltro/env": "0.14.0",
18
- "@voltro/plugin-flags": "0.14.0",
19
- "@voltro/plugin-multitenancy": "0.14.0",
20
- "@voltro/protocol": "0.14.0",
21
- "@voltro/runtime": "0.14.0",
22
- "@voltro/sql-postgres": "0.14.0",
15
+ "@voltro/cli": "0.16.0",
16
+ "@voltro/database": "0.16.0",
17
+ "@voltro/env": "0.16.0",
18
+ "@voltro/plugin-flags": "0.16.0",
19
+ "@voltro/plugin-multitenancy": "0.16.0",
20
+ "@voltro/protocol": "0.16.0",
21
+ "@voltro/runtime": "0.16.0",
22
+ "@voltro/sql-postgres": "0.16.0",
23
23
  "effect": "^3.21.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.14.0",
26
+ "@voltro/testing": "0.16.0",
27
27
  "typescript": "^5.7.0",
28
28
  "vitest": "^3.0.0"
29
29
  }
@@ -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.14.0",
16
- "@voltro/database": "0.14.0",
17
- "@voltro/env": "0.14.0",
18
- "@voltro/plugin-audit": "0.14.0",
19
- "@voltro/plugin-governance": "0.14.0",
20
- "@voltro/protocol": "0.14.0",
21
- "@voltro/runtime": "0.14.0",
15
+ "@voltro/cli": "0.16.0",
16
+ "@voltro/database": "0.16.0",
17
+ "@voltro/env": "0.16.0",
18
+ "@voltro/plugin-audit": "0.16.0",
19
+ "@voltro/plugin-governance": "0.16.0",
20
+ "@voltro/protocol": "0.16.0",
21
+ "@voltro/runtime": "0.16.0",
22
22
  "effect": "^3.21.2"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.14.0",
25
+ "@voltro/testing": "0.16.0",
26
26
  "typescript": "^5.7.0",
27
27
  "vitest": "^3.0.0"
28
28
  }