@voltro/ui-shadcn 0.64.0 → 0.65.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 (2) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -39,6 +39,73 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.65.0] — 2026-09-04
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/plugin-ai-flows** — `@voltro/plugin-ai-flows` declares `internal: true` on its `flow.run` workflow, so it no longer becomes a start rpc. Every start is server-side and always was: the plugin's own guarded procedures, the cadence tick, and `chainTo`'s `workflows.child`. There is no meaningful client start — the payload carries `runId`, the id of a row the caller must already have created.
47
+
48
+ Leaving the decision off was not a lax default. An app installs the plugin by re-exporting the descriptor from its own `flow.run.workflow.tsx`, which makes it one of the app's DISCOVERED workflows — exactly the set the boot gate judges — so under `security.defaultDeny` (on by default) the app's boot was REFUSED, naming a workflow the app had no way to decide about. A first-party descriptor must satisfy the framework's own gate, and a test now holds every workflow any first-party package ships to that.
49
+
50
+ Migration: start a flow through the plugin's guarded procedures. Server code is unaffected.
51
+
52
+ **`voltro update` carries you across this** — codemod `0.65.0/01_ai-flows-run-is-internal`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.65.0).
53
+ - **@voltro/runtime, @voltro/protocol, @voltro/cli, @voltro/voltro** — `EffectStore` gains `one`, `maybeOne` and `first` — the same single-row reads `ctx.store` offers, taking a builder or a descriptor.
54
+
55
+ Their absence made two `voltro doctor` rules mutually unsatisfiable: `row-not-found` prescribes `.maybeOne(builder)`, `store-effect-lift` prescribes `yield* EffectStore`, and the Effect surface had no `.maybeOne`. Following both produced `Property 'maybeOne' does not exist on type 'EffectStoreOps'`. Nothing was hard about it — the object the service closes over has always been the full fluent store, and every call site was already casting to say so; only the declared type was narrower. `transactional` inherits the wider surface.
56
+
57
+ `one` fails with `NoRowFound` on its own channel rather than collapsing into `StoreOperationFailed`, for the reason the catch boundary already gives about `TenantRowNotFound`: stringifying a deliberate, catchable refusal is how it stops being one. There is no `onMissing` on the Effect surface — `Effect.catchTag('NoRowFound', …)` says the same thing and keeps the channel exact.
58
+
59
+ `NoRowFound` moves to `@voltro/protocol` (re-exported from `@voltro/runtime`, so nothing that imported it moves). The agent guide already said it was declarable in a descriptor's `error:` union, and a descriptor is browser-loaded while `@voltro/runtime` is on the browser-safety guard's server-only list — so following that sentence produced a refused boot. The package it sat in was the only thing making the guidance false.
60
+
61
+ **`voltro update` carries you across this** — codemod `0.65.0/02_effect-store-and-guard-unions`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.65.0).
62
+
63
+ ### Added
64
+
65
+ - **@voltro/protocol, @voltro/runtime, @voltro/voltro** — `guards: [{ authenticated: true }]` — the access decision that says "the caller must have an identity", and nothing narrower.
66
+
67
+ It was the third case the vocabulary could not express, and every substitute was wrong in a different way. `{ scope: … }` refuses every real user in an app that issues no raw scopes, which is what authority resolved per team in the executor looks like. A relationship guard has no tuple to match on the first call, when the resource does not exist yet. And `openAccess` is untrue wherever the auth fallback is a deliberate anonymous subject — the honest reason would read "whoever can open the socket may spend our model budget". Apps were writing a resource policy whose entire content was "you are logged in".
68
+
69
+ `checkGuards` already distinguished this internally: its return type has always been `ScopeError | Unauthenticated | null`. The guard makes the distinction sayable. It ENFORCES, so it satisfies `security.defaultDeny` — unlike `openAccess`, which declares that nothing is checked — and it composes: every entry in `guards:` must pass.
70
+
71
+ Both enforcement loops carry it, the synchronous one and the Effect one that runs whenever a policy or resource-scoped guard is present. The approval serialiser stores it too: a variant it dropped would be an identity requirement that quietly stopped applying the moment a decision went through an approval.
72
+
73
+ The golden churn here is one member added to `AnyGuardSpec` / `AnyCheckSpec`. Nothing already written changes meaning; only a switch that is EXHAUSTIVE over the union needs an arm, and that case is carried by the manual codemod `0.65.0/02_effect-store-and-guard-unions` alongside the store-layer change it ships with.
74
+
75
+ ### Changed
76
+
77
+ - **@voltro/cli** — Four `voltro doctor` corrections, each where the rule's premise was narrower than the code it read:
78
+
79
+ - `n-plus-one` follows a dependent chain through ORDINARY bindings. It compared the next read's text against the names earlier reads were DIRECTLY assigned to, so `const row = rows[0]` between two reads made the second look independent — and a finite chain whose next key comes out of the previous row was reported as an N+1 you are writing, which `Effect.all` cannot fix. - `input/uncapped-array` reads the `input:` expression alone, plus the schemas it names. It scanned the whole file, so a descriptor whose input holds no array and whose output returns `items: Schema.Array(Item)` was reported — and a `maxItems` on a RESPONSE bounds nothing a request can ask for. - `mutation/target-op-mismatch` treats `target: [...]` as the set of allowed ops per table. Comparing each declared target ALONE asked "does the executor do anything but insert", answered yes because delete was also declared, and reported a complete declaration. - `bulk-write-in-loop` recommends only APIs that exist. It named `insertManyIgnore` and `upsertMany`, neither of which the store has; `insertIgnore` and `upsert` resolve a conflict per row and have no set-based form, so the advice is now to keep the loop inside one transaction and bound the input.
80
+ - **@voltro/cli** — `voltro update`'s codemod preview splits the count in the summary line: `6 in range for 0.63.0 → 0.64.0 — 6 print written steps for you to apply`. The per-entry markers were already there and the legend explained them, but both sit BELOW the count, so "6 codemods" formed the expectation of six automatic rewrites before the reader reached either. Six manual codemods is six pieces of hand work, and that is the number somebody schedules an afternoon around.
81
+
82
+ ### Fixed
83
+
84
+ - **@voltro/cli** — `open-reason-unverified` had three defects that between them let it be silenced by rewording while missing the case it exists for:
85
+
86
+ - The reason literal was matched with one quote class that forbade every quote character INSIDE the string, so a reason naming its guard in backticks did not parse and the rule returned without opening the executor at all. - `can` is a framework guard name AND an ordinary English word, so any reason containing "anyone can call it" named a guard the executor did not have. A lowercase word now counts only when written as code — `can()` or in backticks. - The executor test was token PRESENCE, which the import line alone satisfies. It requires a CALL, the same shape the sibling rule already used.
87
+
88
+ `input/uncapped-array` covers `*.query.ts` as well. A read does not write, which is what the old wording was about, but that does not make it bounded: a hundred thousand ids in one `getByIds` is the caller choosing the size of the query, and the body limit bounds bytes rather than entries.
89
+
90
+ `store-effect-lift` names both preconditions its promise depends on — that a typed error only reaches the caller once the descriptor DECLARES it, and that the class comes from `@voltro/protocol`. Naming a benefit and neither precondition is how a rule earns a large diff with nothing observable at the end of it.
91
+ - **@voltro/runtime** — An `.encrypted()` column written through `upsert` is bound to the row that RECEIVES it. On the conflict path that row is the existing one, whose id is not the id the caller offered, so the ciphertext went to disk authenticated against an id that row does not have — and AES-GCM then refused it: `FieldDecryptionError … Unsupported state or unable to authenticate data`.
92
+
93
+ The damage was not the failed call. The row keeps the mis-bound value, so every later read of it throws too, and the write reports success on the way in. The conflict is now resolved before the encrypt (`upsertDestinationId`, shared by both codec wrappers), which costs one extra read and only for a call that actually writes a secret.
94
+
95
+ It bites wherever the row carries an id at encrypt time — always on the memory store, and on every SQL dialect when the id is the caller's rather than the database's, since `EXCLUDED.<col>` / `VALUES(<col>)` / `MERGE` all carry the offered bytes onto the existing row.
96
+
97
+ `updateMany` through the BOOT store now refuses an `.encrypted()` column instead of writing a binding nothing can authenticate: a set-based write has no row, and the request-scoped store is the one that expands it per row.
98
+ - **@voltro/cli** — `.framework/dist/server/` — the SSR bundle and the precompiled app config — is marked `{"type":"module"}` like the api and start bundles already were, so a production `voltro start` no longer opens with `MODULE_TYPELESS_PACKAGE_JSON` for an artefact the app did not write. All three directories go through one `writeEsmPackageMarker`, and a guard now asks every module that runs a bundler whether it marks what it wrote.
99
+ - **@voltro/cli** — `voltro new workflow` scaffolds a descriptor that declares `internal: true`, with the three options in a comment above it. The template emitted no access decision at all, so the file it wrote refused the app's own boot under `security.defaultDeny` — a scaffold whose first act is to break the build.
100
+ - **@voltro/database, @voltro/runtime, @voltro/protocol, @voltro/voltro** — `StoreOperationFailed.cause` no longer carries the driver's sentence or a stack trace. It was `String(error)` on a `FiberFailure`, which is the message AND the stack — so an absolute server path reached every caller that declared the error, and every subscriber on a stream carrying it.
101
+
102
+ The words were the worse half. `@voltro/database`'s own header already states why a driver message may not be forwarded: postgres attaches `Failing row contains (…)` — the entire row, `.sensitive()` columns included — to a not-null and a check violation, mysql echoes the duplicate value, mssql the truncated one. That rule was written for `ConstraintViolation` and the error beside it broke it.
103
+
104
+ `wireSafeCause` builds the field now: the constraint KIND and its constraint/column NAME, or the driver's code, and nothing that was ever a value. The full detail is logged server-side instead, so diagnosis is not traded away for the fix.
105
+ - **@voltro/cli** — The inspect action behind the dashboard's workflow "resume" refuses a run that is not suspended, instead of reporting a resume that did not happen. The engine resumes only a run carrying a terminal `Suspended` reply; the action called it, ignored the answer, flipped the row to `running` and recorded a `run-resumed` event regardless. For a run already stuck in `running` — the state somebody presses that button in — every visible sign said it had worked, including a fabricated entry in the timeline of the run being investigated.
106
+
107
+ ---
108
+
42
109
  ## [0.64.0] — 2026-09-04
43
110
 
44
111
  ### ⚠ BREAKING
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/ui-shadcn",
3
- "version": "0.64.0",
3
+ "version": "0.65.0",
4
4
  "description": "Voltro's first-party shadcn/ui kit: Tailwind v4 design tokens (light + dark), 30+ primitives, layout compositions, styled widgets for the @voltro/ui seam, and the canonical theme/language preference-cookie helpers.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -49,7 +49,7 @@
49
49
  "@radix-ui/react-dropdown-menu": "^2.1.24",
50
50
  "@radix-ui/react-toggle": "^1.1.18",
51
51
  "@radix-ui/react-toggle-group": "^1.1.19",
52
- "@voltro/ui": "0.64.0",
52
+ "@voltro/ui": "0.65.0",
53
53
  "class-variance-authority": "^0.7.1",
54
54
  "clsx": "^2.1.1",
55
55
  "shiki": "^4.4.3",