@voltro/ui-shadcn 0.71.0 → 0.71.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -39,6 +39,58 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.71.1] — 2026-09-14
43
+
44
+ ### Added
45
+
46
+ - **The doctor flags a declared error thrown where the channel discards it** — `@voltro/cli`
47
+
48
+ `Effect.promise` and `Effect.sync` turn every throw into a DEFECT, not a failure. So a transaction lifted with `Effect.promise` whose body throws an error the descriptor declares in its `error:` union hands the caller a generic failure instead of the message the contract promised — and the code reads exactly like the working form, so nothing at the call site gives it away.
49
+
50
+ `promise-swallows-typed-error` is a contradiction between a declaration and the code, the same shape as `open-reason-unverified` rather than a matter of taste, and it is precise about it: only errors the PAIRED DESCRIPTOR declares, and never `Effect.tryPromise`, whose `catch` is exactly the mapping from a throw onto the failure channel. The fix it names is to return the work as an Effect — `store.transactional((tx) => Effect.gen(…))` with `yield* Effect.fail(…)`.
51
+ - **`storageInstanceName()` — the value an upload ticket has to carry** — `@voltro/plugin-storage`
52
+
53
+ `signUploadTicket` takes `instance: string`, and the upload route compares it against the plugin's own instance — so a wrong value is not a type error, it is a bare `401` on every upload. The value was always derivable (`storagePlugin(options).name` is the same expression), but nothing said so and the base name is not exported, which left a caller minting a ticket outside the plugin to guess at a `string`.
54
+
55
+ `storageInstanceName(options)` returns it, from the same derivation the plugin registers itself under. Pass the options the installation was built with: `alias` and `name` both move the instance, and a ticket minted for the default name does not open a door installed under an alias.
56
+
57
+ ### Fixed
58
+
59
+ - **The doctor recognises a call that carries explicit type arguments** — `@voltro/cli`
60
+
61
+ A callee may spell its type arguments between the name and the paren — `inSet<Record<string, unknown>, string>('parentId', ids)` — and every rule asking whether a function was CALLED read that as a call nobody wrote. The direction it failed in is the quiet one: `n-plus-one` exempts a read whose predicate is `inSet` / `paginateBy` / `paginateById`, so a breadth-first tree walk issuing ONE query per level was reported as one query per row. The advice for that finding is to gather the keys and read the set once, which such a file already does — there is no edit that clears it.
62
+
63
+ Seven call-site patterns now share one fragment, and a guard over the doctor's own source fails on the eighth spelled by hand. The spread was the point: the same defect had already cost `unexercised-row-filter` its `setRowFilter<Ctx>({ … })` registrations, and a second local fix would have left the next one standing.
64
+ - **A replaced connection setup reports superseded instead of inactive** — `@voltro/client`
65
+
66
+ `useConnectionSetup(...).activate` is a join-exactly-once: a second call clears the first, whose promise then rejects. It rejected with `inactive` — the same reason a setup that is genuinely gone gives — so an app could not tell "my own successor is running" from "there is no such setup". React StrictMode repeats every mount effect in development, which makes the first case the ordinary path, and a route showing the generic message reported that the session did not exist while the second join was succeeding.
67
+
68
+ The reason is now `superseded`, carried on the activation's abort signal so the barrier that raises it can read it. `inactive`, `connection-unavailable`, `connection-changed` and `timeout` are unchanged, and the cleanup order is unchanged — only the name of this one outcome.
69
+ - **A form rule reads the handler, not the combinator's plumbing** — `@voltro/cli`
70
+
71
+ `voltro doctor`'s form rules asked "are these two things in the same function", and `Effect.promise(() => …)` answered no: the thunk the combinator requires is a function, so a hand-rolled pager whose `limit + 1` sat inside the lift and whose `hasMore` sat outside it counted as two functions and reported clean. The rule did not go wrong loudly — it went silent, which reads exactly like a codebase that fixed itself. Measured across one upgrade: six findings became zero with the pagers untouched, and came back the moment the same reads were written without the wrapper.
72
+
73
+ `Effect.promise`, `Effect.sync`, `Effect.suspend` and `Effect.tryPromise`'s `try` are transparent to that question now. A function someone wrote for its own sake is still a boundary — a `limit + 1` in one helper and a `hasMore` in another do not pair up.
74
+
75
+ `hand-cursor` and `sequential-reads` are the two rules this restores.
76
+ - **A text column declared as the sole primary key no longer blocks every migration** — `@voltro/database`, `@voltro/cli`
77
+
78
+ `voltro migrate` could not converge on an EMPTY database as soon as a table spelled its key `id: text().maxLength(64)` with `.primaryKey(['id'])`. Introspection reports a single-column primary key named `id` as the `id` column KIND and as unique — which is what `id()` declares — so the plan re-emitted an `alter-column-type` (classified lossy, therefore blocking) and a `drop-unique` against a table the same migration had just created. No fingerprint was recorded, correctly, and the next run started from the top.
79
+
80
+ Seven framework tables carry that spelling, so a first install could not start once it reached the workflow engine, the storage plugin or tenant namespaces. The planner and the fingerprint now treat the two spellings of one primary key as one column (`sameIdSpelling`), and neither normalises the snapshot: a `.maxLength()` change on such a key is still planned, and the applier still emits the DDL the declaration spells.
81
+
82
+ Every framework-declared table is now applied to a real database and re-planned as part of the framework's own verification, so a declaration whose shape the diff cannot close is caught before it reaches a release rather than on someone's first install.
83
+ - **The built-in `actors` columns carry their own exposure class** — `@voltro/database`
84
+
85
+ `actors.kind` is `.safe()` and `actors.displayName` is `.sensitive('fullName')`. Neither carried a class, and an app cannot give them one — `actors` is framework-provided and declaring it is refused — so a masked export stayed fail-closed on two values the framework already knows the answer for, and `voltro doctor`'s sensitivity rule asked apps to classify columns they do not own.
86
+ - **`store-effect-lift` is silent where the failure channel is pinned** — `@voltro/cli`
87
+
88
+ Two bodies pin their failure channel to `never`: a `step({ execute })` with no `error:` schema (`Error` defaults to `Schema.Never`) and a `*.tool.tsx` handler, whose contract is `Effect<string, never, unknown>`. `yield* EffectStore` fails with `StoreError`, so the rule's advice did not typecheck there — and `Effect.promise` is the right statement in those bodies anyway: a store failure in a step is a defect, which is what the retry policy is built on. Declaring an `error:` on the step widens the channel, and there the rule still fires.
89
+
90
+ The advice itself now names what is on the other side. It said `yield* EffectStore` and stopped, so a reader who went looking had to rediscover the surface: it carries every operation `ctx.store` does, and two of them do not read like their Promise spelling. The set-based `deleteMany(t, { where })` is PHYSICAL — it is the Effect form of the fluent `delete(t).where(p).hard()`, while the keyed `delete(t, id)` is the one a `softDelete()` mixin redirects. And `one` fails with `NoRowFound` on its own channel, so `Effect.catchTag('NoRowFound', …)` maps it to your own error without a `maybeOne` + null check.
91
+
92
+ ---
93
+
42
94
  ## [0.71.0] — 2026-09-14
43
95
 
44
96
  ### ⚠ BREAKING
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/ui-shadcn",
3
- "version": "0.71.0",
3
+ "version": "0.71.1",
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",
@@ -52,7 +52,7 @@
52
52
  "@radix-ui/react-toggle-group": "^1.1.19",
53
53
  "@shikijs/langs": "^4.4.3",
54
54
  "@shikijs/themes": "^4.4.3",
55
- "@voltro/ui": "0.71.0",
55
+ "@voltro/ui": "0.71.1",
56
56
  "class-variance-authority": "^0.7.1",
57
57
  "clsx": "^2.1.1",
58
58
  "shiki": "^4.4.3",