@voltro/ui-shadcn 0.73.0 → 0.74.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.
- package/CHANGELOG.md +130 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,136 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.74.0] — 2026-09-18
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **An app can assert it still agrees with the framework** — `@voltro/cli`
|
|
47
|
+
|
|
48
|
+
`@voltro/cli/contract` publishes the three things an app needs to check itself against the framework's own implementation rather than against a copy of it:
|
|
49
|
+
|
|
50
|
+
- `assertBrowserSafeRpcGroup(path)` — the generated rpc group is loaded value-level by the browser, so a descriptor that transitively imports a server-only module drags the schema graph into the client bundle. `voltro dev` runs this at boot; until now an app could not ask the same question from its own suite, before a deploy. - `observeInspectResponse(path, response, fleetSize)` — the envelope every inspect answer is wrapped in. An app whose output schema decodes an inspect payload is making a claim about this function's shape, and a test that rebuilds the envelope by hand asserts against its own reconstruction instead. - `buildClusterInspect(deps)` — the cluster snapshot as the framework assembles it, for the same reason.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { assertBrowserSafeRpcGroup } from '@voltro/cli/contract'
|
|
54
|
+
|
|
55
|
+
it('keeps the generated rpc descriptor graph browser-safe', async () => {
|
|
56
|
+
await expect(assertBrowserSafeRpcGroup(rpcGroupPath)).resolves.toBeUndefined()
|
|
57
|
+
}, 30_000)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
One consequence is worth stating rather than leaving to be discovered: `observeInspectResponse` is default-deny over its route-scope table — a path with no declared scope is answered with a 500 naming the fix, not wrapped bare — and publishing it makes that part of the public promise.
|
|
61
|
+
|
|
62
|
+
The surface is for asserting, not for building on: nothing in it mounts a route, serves a request or writes a file.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **A file migration records the body that ran, so a later edit to it is visible** — `@voltro/database`, `@voltro/cli`
|
|
67
|
+
|
|
68
|
+
An applied `migrations/<timestamp>_<slug>.ts` file was looked up by its id and nothing else:
|
|
69
|
+
|
|
70
|
+
```sql
|
|
71
|
+
SELECT id FROM _voltro_migration_plans WHERE source = 'file'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
So its body could be rewritten after it had been applied and nothing would say so. The file then reads like a description of what ran against the database, and is not one — which is the state every other migration tool checks for, and which the framework's own `*.migration.ts` runner has recorded a hash for and warned about since it shipped. The file runner wrote an empty string into the same column.
|
|
75
|
+
|
|
76
|
+
It records the body hash now, and reports a body that no longer matches:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
file migration: the body on disk is not the one that was applied —
|
|
80
|
+
the file no longer describes this database id=20260915_054800_vacation_days
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Two things about it are deliberate:
|
|
84
|
+
|
|
85
|
+
- **A row written before this shipped carries an empty hash, and that reads as "cannot compare", never as "changed".** The other way round, the first run after upgrading would report drift for every migration a database has ever applied — the loudest possible way to make a true warning ignorable. - **It warns; it does not fail.** Nothing can be re-run to fix it: the change is in the database, and the edit is usually deliberate. What was missing is that anyone could tell.
|
|
86
|
+
|
|
87
|
+
The hash is one function shared by both runners rather than spelled twice — a hash is only comparable to one computed identically, and two definitions of "the hash of this file" become two answers the moment either is touched.
|
|
88
|
+
- **A new unique column arrives with its unique, and a part-applied schema says so** — `@voltro/database`
|
|
89
|
+
|
|
90
|
+
Adding a NOT NULL column that declares `.unique()` produced a migration that executed without error and then failed its own convergence proof:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
applyPlan: the migration did not converge. 14 operation(s) were executed without
|
|
94
|
+
error, but re-planning against the live schema still finds 1:
|
|
95
|
+
- add-unique _voltro_ai_usage.receiptKey
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The second run succeeded with exactly that one operation, because by then the column existed.
|
|
99
|
+
|
|
100
|
+
A column-level `.unique()` is a column flag, not a snapshot index, so a unique appearing on an EXISTING column is caught by the column-change comparison. A column the same plan CREATES has no live side to compare against, which leaves the add-column emitter as the only thing that can carry the flag — and it read the flag on one of its three branches. A nullable add rendered `UNIQUE` inline; the NOT NULL branches (with a default, or with a backfill) dropped it. So the column was created, filled and enforced, the unique was never created, and the re-plan proposed it.
|
|
101
|
+
|
|
102
|
+
The three-step add cannot render `UNIQUE` inline — the column is NULL for the length of the backfill — so the emitter now reports the unique as still owed and the applier creates it after the NOT NULL enforcement, through the same per-dialect statements `add-unique` already uses. It is reachable without `.backfill()`: a NOT NULL column with a default and a unique lost it the same way.
|
|
103
|
+
|
|
104
|
+
The failure message changed too, and independently. It ended with *"the schema is unchanged and safe"* in every case, while the applier already knew — twenty lines earlier, on its other failure path — whether the dialect applies a plan atomically. On postgres and mssql the sentence was true; on mysql and mariadb it was not, and a deployment that read it went looking for an untouched schema and found thirteen of fourteen operations committed. It now states which of the two situations it is, and on a non-atomic dialect lists the operations that took effect.
|
|
105
|
+
- **Change-trigger drift is measured against the name postgres stores** — `@voltro/database`
|
|
106
|
+
|
|
107
|
+
On postgres, a table whose name carries an uppercase letter — or is long enough to reach the identifier limit — was reported as having no change trigger on every boot, while its trigger was present and firing. `voltro db apply` then re-installed every one of them, every run, and the count never went down:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
db apply: installing change triggers on 71 table(s)
|
|
111
|
+
db apply: change triggers converged (214 statement(s))
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`CREATE TRIGGER` is emitted with the trigger identifier unquoted — the table name beside it IS quoted, which is what made the asymmetry easy to miss — so postgres stores the name the way it stores any unquoted identifier: downcased, then clipped to 63 bytes. Measured on postgres 17.10:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
activityEvents -> framework_changes_activityevents
|
|
118
|
+
aVeryLongTableNameOverTheLimit… -> framework_changes_averylongtablenameoverthelimit… (63)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The drift detector compared `pg_trigger` against the name the framework DERIVES, so the two could never match for an affected table. It reads the stored form now. The emission is deliberately unchanged: `CREATE TRIGGER` and its `DROP TRIGGER IF EXISTS` fold identically and therefore already agree with each other, so quoting only the create would have left every existing database's trigger undroppable and added a second one beside it.
|
|
122
|
+
|
|
123
|
+
The truncation half is reachable with no uppercase letter at all, so folding alone would have fixed the reported case and left the other.
|
|
124
|
+
|
|
125
|
+
The comparison also asks which RELATION carries a trigger, rather than matching on the name alone. Trigger names are unique per table in postgres, not per schema, so two tables whose names differ only in case legitimately carry the same trigger name — and keyed by name alone, one table's trigger answered for the other's. Folding without that would have traded a table wrongly reported as untriggered for a table wrongly reported as triggered, which is the same defect in the more dangerous direction.
|
|
126
|
+
- **voltro db status reports both migration conventions** — `@voltro/cli`
|
|
127
|
+
|
|
128
|
+
Run against the same project, seconds apart:
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
voltro db status . -> no migrations found
|
|
132
|
+
voltro db files . -> no pending file-based migrations (4 already applied)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Neither answer was about the database's state. The two commands walk for different files: `db status` (with `db migrate` and `db rollback`) walks `*.migration.ts` anywhere under the project root, while `db files` — and `db apply`, and the boot path — walks `migrations/<timestamp>_<slug>.ts`. Both conventions are supported; only one of them was ever reported.
|
|
136
|
+
|
|
137
|
+
`no migrations found` is the worst available answer for the other one. It is not a warning that invites a second look, it is a statement that there is nothing to see, and it was being given to a project with four applied migrations sitting in the directory the same CLI had just listed.
|
|
138
|
+
|
|
139
|
+
`db status` now reports both, marks a file migration as `applied` or `pending`, and flags one whose body no longer matches what was recorded when it ran. It says `no migrations found` only when BOTH conventions are empty — and a database it cannot reach is not reported as "no migrations" either, which would be the same false statement one layer along.
|
|
140
|
+
|
|
141
|
+
The count in `db files` was right, for the record: `4 already applied` counts the migrations found in the ledger, not the ones a `skipUnless` precondition skipped. Those are recorded as applied with `skipped: <reason>` in their notes, which is a different thing that the same word describes.
|
|
142
|
+
- **A migration's truncate tolerates a missing table and emits no per-row event** — `@voltro/database`
|
|
143
|
+
|
|
144
|
+
`ctx.schema.truncate(table)` was the one schema operation that did not look at the live schema first. Its neighbours — `dropTable`, `dropIndex`, `dropColumn`, `dropForeignKey`, `renameColumn` — all answer `already-absent` when there is nothing to do, and the documentation states that as a property of every schema operation. So a migration run against a database that predates the table died on the truncate while the `dropForeignKey` on the line above it, naming the same table, had answered `already-absent`:
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
ctx.schema.dropForeignKey('timers', 'roadmapId') -> already-absent
|
|
148
|
+
ctx.schema.truncate('timers') -> SqlError
|
|
149
|
+
Table 'app.timers' doesn't exist (ER_NO_SUCH_TABLE / 1146 / 42S02)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It returns `'applied' | 'already-absent'` now, like the rest.
|
|
153
|
+
|
|
154
|
+
The statement it runs changed too, and that half is the more expensive one. The truncate went through the store's set-based delete, which is `DELETE … WHERE … RETURNING *` **by design**: the rows it returns are the old-images the store fans out as one change event per row. Correct for an application write, wrong for a truncate — emptying a large table pulled every row back to the client and notified every subscriber once per deleted row. It now issues a plain `DELETE FROM <table>` through the migration's own client: no rows returned, no per-row event.
|
|
155
|
+
|
|
156
|
+
`DELETE FROM` rather than `TRUNCATE` is unchanged and deliberate — it is a data statement on every dialect, so it stays inside a transactional migration, where `TRUNCATE` (DDL on the MySQL family and SQL Server) would commit it.
|
|
157
|
+
|
|
158
|
+
### Internal (no consumer-facing effect)
|
|
159
|
+
|
|
160
|
+
- **A test's fixture probe cannot outlive its test** — `@voltro/cli`
|
|
161
|
+
|
|
162
|
+
`frameworkSourceTypo.integration.test.ts` writes a query descriptor into the shared `memory-api` fixture for a few seconds and removes it again. Another task generates from that same fixture — `web-form-post`'s typecheck runs `voltro codegen ../memory-api` — and codegen emits one import per query file it finds. A codegen landing inside that window bakes the probe into `rpcGroup.generated.ts`; the probe is then deleted, and the generated file is left importing a module that no longer exists.
|
|
163
|
+
|
|
164
|
+
Measured rather than reasoned: plant a probe and run `voltro codegen .` and seven references appear; delete the probe and all seven remain. What the next reader gets is `TS2307: Cannot find module './queries/<probe>.query'` in a generated file, with nothing pointing at where it came from.
|
|
165
|
+
|
|
166
|
+
The artefact is gitignored, which is what makes it silent — `git status` reports a clean tree over a poisoned one, so the usual check answers the wrong question.
|
|
167
|
+
|
|
168
|
+
Two changes, for the two ways the residue survives. The `finally` now restores the fixture's generated files from a snapshot taken before the probe was written, so a concurrent codegen is undone along with the probe. And the suite sweeps leftover probe files at start, because a killed process never runs a `finally` — this file already records a saturated machine having the OS kill its child.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
42
172
|
## [0.73.0] — 2026-09-17
|
|
43
173
|
|
|
44
174
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/ui-shadcn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.74.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",
|
|
@@ -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.
|
|
55
|
+
"@voltro/ui": "0.74.0",
|
|
56
56
|
"class-variance-authority": "^0.7.1",
|
|
57
57
|
"clsx": "^2.1.1",
|
|
58
58
|
"shiki": "^4.4.3",
|