@voltro/plugin-atlassian 0.50.0 → 0.51.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 +160 -0
  2. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -39,6 +39,166 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.51.0] — 2026-08-24
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/data-transfer, @voltro/cli** — The asset counts reported references as if they were blobs. `_voltro_storage_refs` holds one row per reference and several rows legitimately name one key, so a capture of 57 rows over 16 keys wrote `count: 57` into the stamp beside an `assets/` directory holding 16 files, and the restore reported "57 blob(s) restored" while 16 objects appeared. Nothing was lost; what was lost is the ability to check. Anyone answering "are all the blobs there?" after a restore compared the stamp's number against one they counted and found a 3.5x gap that was not one.
47
+
48
+ A key named by several references is now fetched once rather than downloaded, hashed and discarded once per row, and the three numbers are stated separately: `references` (rows enumerated), `count` (distinct keys), `objects` (distinct sha256 bodies), with `totalBytes` and `objectBytes` beside them. The stamp's existing fields keep their names and now mean what a reader always took them for; the new ones are optional, so a stamp written before them still parses.
49
+
50
+ **Breaking on one export.** `restoreAssetsFromCas` returns `{ count, objects }` instead of a bare `number` — one number could not answer both questions, which is the defect. `count` is what the old value was, so the migration that changes nothing is `.count`.
51
+
52
+ **`voltro update` carries you across this** — codemod `0.51.0/03_restore-assets-returns-counts`. 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.51.0).
53
+ - **@voltro/i18n** — `<I18nProvider>` takes `timeZone` as its own prop, and `intlConfig` no longer accepts one.
54
+
55
+ `intlConfig` exists to forward props to react-intl UNMODIFIED, and for every other member of `IntlConfig` that is the right shape. `timeZone` is the one member this package's own formatters read — and they did not read it: `useFormatDate` built `Intl.DateTimeFormat` itself and took only the locale from the provider. So a zone passed through `intlConfig` configured `<T>`'s ICU dates and NOT the `useFormatDate()` beside them. Measured under one provider, one instant (`2026-08-24T23:30:00Z`), `locale: 'de'`, `intlConfig: { timeZone: 'Europe/Berlin' }`, process zone UTC: react-intl rendered `25.08.26, 01:30` and the hook rendered `24.08.26, 23:30`. A different hour, and a different day.
56
+
57
+ The zone has to be a prop this package can see. It is validated once at the provider (an unusable zone — a stale cookie, a typo, a runtime with a trimmed ICU — is dropped, because `Intl` THROWS on an unknown zone and `useFormatDate` catches, which would degrade every timestamp in the app to a raw `Date` string). It is what the new `useTimeZone()` reports. And it is what the framework fills per request.
58
+
59
+ **Migration:** `intlConfig={{ timeZone: 'Europe/Berlin' }}` → `timeZone="Europe/Berlin"`. The codemod does it, including the case where the zone was the bag's only member. An `intlConfig` naming a variable is reported by file and line rather than guessed at — the property would otherwise stop being read with nothing red anywhere.
60
+ - **@voltro/cli** — A native restore whose bookkeeping store would not open ran anyway, with no in-progress marker and no word about it. `nativeBookkeeping` was `try { … } catch { return undefined }`, and that `undefined` guarded every branch below — including the refusal for a marker that could not be written. So the failure removed the precaution AND the sentence that would have reported it missing, and `markerState` was never computed, defaulting to "held".
61
+
62
+ What it produced was worse than silence: a failed restore printed "This database is now in an unknown state and the next boot will REFUSE, by design" over a database with zero marker rows. The next boot did not refuse, and `voltro data clear-replace-marker` had nothing to clear. The trigger is not exotic — wrong credentials, an unreachable database, a missing env var, no `app.config.ts` from here — and a restore is the operation you run against a target that is already unwell, so the guard fell away exactly when it was needed.
63
+
64
+ The reason now travels instead of being caught and dropped. A restore that cannot write the marker REFUSES and names which of the two reasons it was (the store would not open, or the table is not there); those were two separate refusals and are now one, because they are one decision for the operator. `--no-marker` is the deliberate way past it and warns every time. And the `recorded:` line no longer reports a local failure as a property of the target — it says where the failure was.
65
+
66
+ **This changes an exit code.** A restore that could not write the marker used to exit 0; it now exits 1. Two invocations are affected — the bookkeeping store will not open, or there is no `app.config.ts` from the working directory — and the second surprises people, because `restore` reads its target from the environment and so looks like it needs no project. It does not, for the dump; it needs one for the marker. `--no-marker` is the deliberate way through and warns every time. The note ships under `reach: 'beyond-source'` because the affected invocations live in cron entries, CI jobs and runbooks rather than in TypeScript.
67
+
68
+ **`voltro update` carries you across this** — codemod `0.51.0/02_restore-refuses-without-marker`. 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.51.0).
69
+
70
+ ### Added
71
+
72
+ - **@voltro/i18n, @voltro/cli, @voltro/voltro** — `timeZone` in the web `app.config.ts` — the zone every date/time formatter renders in, resolved per request and published so the client agrees.
73
+
74
+ A formatter is deterministic given the value, the locale, the zone and the clock. The locale already came from the provider and was already agreed across the hydration boundary — the server publishes it as `<html lang>` and the client reads that attribute rather than `navigator.languages`, precisely because the browser's own answer can differ from what the server saw. The zone had no such source. `Intl` fell back to the zone of whichever runtime was formatting: the pod on the server (UTC on a container with no `TZ`), the viewer's machine in the browser. Every server-rendered timestamp was therefore a hydration mismatch waiting for a wide enough offset, and across midnight it was a different calendar day.
75
+
76
+ timeZone: 'Europe/Berlin' // one zone for every viewer timeZone: 'viewer' // per request, from the `voltro:tz` cookie defaultTimeZone: 'UTC' // before the viewer's zone is known
77
+
78
+ Whatever it resolves to is stamped on the document as `<html data-voltro-tz>`, and the generated client entry reads that attribute. Both sides then format against one value — which is the property that removes the mismatch, whether or not the value is the viewer's true zone: being wrong together is repairable after mount, being different is not.
79
+
80
+ Under `'viewer'` the framework injects a script that seeds `voltro:tz` from the browser when the cookie is absent, so the server renders in the viewer's zone from the second request with no login. It never overwrites an existing value — the APP is the authoritative writer, at login, from the zone it holds for the signed-in user (`TIMEZONE_COOKIE` and `isSupportedTimeZone` are exported for that). Unset, nothing changes: each runtime keeps using its own zone, and `useTimeZone()` returns `undefined` to say so.
81
+
82
+ This is the RENDER zone. The server-side compute zone — what `startOfDay` resolves against inside a handler — is still `@voltro/datetime/context`'s seam, unwired.
83
+
84
+ **`apiSurface: compatible`** covers the two golden lines that moved, and they are the same change twice: `makeSsgWrap`'s returned wrapper, and the `wraps` record on the SSG shell input, each gained an OPTIONAL second parameter (the per-request zone + render instant; the wrapper is built once per locale, so they cannot live in the factory). A function with an optional extra parameter is assignable wherever the one-parameter type was expected, so no call site that compiled stops compiling — and both are the framework's SSG bridge, documented as never imported by app code. Everything else this release adds to these packages is a pure addition; the one genuine break in `@voltro/i18n` is the `timeZone` prop, which has its own entry and its own codemod.
85
+
86
+ ### Changed
87
+
88
+ - **@voltro/cli** — A `BREAKING` entry's changelog footer now tells a reader who pins versions by hand how to print the codemod's note without upgrading anything:
89
+
90
+ voltro update --codemods-only --from <your current version> --dry-run
91
+
92
+ The footer used to stop at the codemod's id, which is enough for anyone who runs `voltro update` and nothing at all for anyone who does not. A deployment said so plainly: they pin every `@voltro/*` version from their own container scripts, have never run the command, and `CHANGELOG.md` out of the tarball is the only channel anything reaches them through. So a note deliberately filed under an unreached version — our one mechanism for correcting guidance that can no longer be corrected in place — reached that population not at all, and the id told them a fix existed without telling them what it was.
93
+
94
+ No new surface: every flag in that invocation is already parsed, which is what lets `check-message-apis.mjs` verify the line rather than trust it.
95
+ - **@voltro/cli** — Two fingerprint labels now say what they compare.
96
+
97
+ The restore's skew warning said the backup's schema "differs from what this code declares". It does not: the value it compares against is the TARGET database's live schema, read by introspection at restore time. Bringing a target to the backup's shape makes the warning disappear while the declared fingerprint is a third value entirely, which is how the mislabel was caught. The comparison is the useful one and is unchanged; the sentence sent readers looking for a code change where a database differed.
98
+
99
+ `voltro db plan` prints `fingerprint: live … · declared …` instead of `from … → to …`, plus a line saying the two are not meant to match. A hash of a live database never equals the hash of the declaration it came from — introspection cannot recover generated expressions, `maxLength` or sensitivity markers — which is why `db drift` keeps a separate live baseline. Printed as `from → to`, `0 operations` under two differing hashes read as a contradiction.
100
+
101
+ ### Fixed
102
+
103
+ - **@voltro/cli** — `voltro check` reported a reactivity CHANNEL as a missing table, at `error` severity — so it set the exit code:
104
+
105
+ ✗ error reference/dangling-source query(presence.list) reads table 'channel:presence' which does not exist fix: declare a 'channel:presence.entity.ts' table or fix the query's source
106
+
107
+ A `source:` entry is a table name OR a channel's routing key (`channel:<name>`), and every rule resolved entries against the table set. The advice cannot be followed — a channel exists precisely because no table is meant — and because it is an error rather than a warning, `voltro check` could not be a CI gate for any app that uses a channel. That includes an app whose only channel comes from `@voltro/plugin-presence`, whose own `presence.list` declares one: a first-party feature meeting a rule that did not know about it, inside a first-party plugin.
108
+
109
+ Channels are filtered in the ONE helper every table rule reads, rather than at each rule, because a per-rule filter is how the next rule joins without one. The same cause was live one rule over: `observed/declared-but-unobserved` reported "declares source 'channel:presence' but never read it while running" for every exercised procedure that declares a channel. Both are covered, each with a negative control — a filter that dropped the whole source list would have silenced the rules instead of narrowing them.
110
+ - **@voltro/data-transfer** — A native dump no longer carries `_voltro_data_transfers`, for the same reason it stopped carrying the in-progress marker one release ago. The restore opens its own run row there BEFORE the tool runs; the dump then dropped the table mid-flight, and the update recording the outcome wrote into a table that no longer held the row. Measured downstream: after a deliberately failed native restore, `voltro data transfers` showed no restore at all — only the `backup` row the dump had carried over from the SOURCE database. The command that answers "did the restore finish" could not see the run asking the question.
111
+
112
+ Exactly two tables are excluded and the line is deliberate: a native restore into the same deployment should bring the migration ledger, the stored plans, the CDC offsets and the schedule claims, because they describe the data being restored. These two describe the RESTORE, and a record of an operation must not be overwritten by the operation it records. Covered per dialect against real servers and real vendor tools, including a non-vacuity check that a table which SHOULD travel still does.
113
+ - **@voltro/i18n, @voltro/cli** — `useRelativeTime` used `Date.now()` as its base, which under SSR is two different numbers. The server rendered at T and wrote "3 minutes ago" into the HTML; the browser hydrated at T+Δ and rendered "4 minutes ago" whenever a unit boundary fell in the gap. The gap is network latency, so it reproduced on a slow connection and never on the developer's machine, and it had nothing to do with timezones — a correctly zoned app hit it just the same.
114
+
115
+ The server states its render instant (`<html data-voltro-now>`, `renderedAt` on the provider), the first client render uses that same number, and the clock goes live once hydration commits. Server markup and hydration markup are therefore identical BY CONSTRUCTION — the property `await.tsx` and `deferred.ts` already hold, rather than `suppressHydrationWarning`, which would hide a real mismatch along with this one. An explicit `{ now }` still wins.
116
+
117
+ The mount state lives in the provider, not in the hook: a table of ten thousand rows would otherwise pay a state hook and a passive effect each to learn one fact that is true for the whole document. An app that never renders on the server publishes no stamp and takes no second render pass.
118
+ - **@voltro/cli** — `closeNativeRun` writes the transfer row BACK when the restore's own artefact dropped the table it lives in, instead of issuing an `UPDATE` that matches nothing and returning happily. Excluding `_voltro_data_transfers` from our own dumps shortens that window; it does nothing for a dump taken before that change, for a hand-made one, or for mssql and sqlite, whose restores have no per-table exclusion at all. The write-back covers every dialect and every artefact, which is why it is the rule and the exclusion is the optimisation.
119
+
120
+ The row is read back rather than trusted — an update that matched nothing is indistinguishable from one that matched — and a read that itself fails writes nothing, because a duplicate row invented on a guess is its own defect in a history somebody reads under pressure.
121
+ - **@voltro/cli** — A prerendered page shipped the shell's baked `lang="en"` whatever locale it was rendered in.
122
+
123
+ `voltro dev` and `voltro start` both set `<html lang>` per request; the prerender never did. So a `/de/...` artefact — rendered with the German catalog, handed `locale: 'de'` in its `meta` — served `<html lang="en">`. That attribute is what a screen reader pronounces in, what Chrome offers to translate FROM, and what hyphenation uses, so the failure was silent to whoever shipped it and loud only to the people it excluded. The same shape as the 0.30.0 cookie-name drift, one document path over.
124
+
125
+ It surfaced while giving the zone somewhere to travel: `<html lang>` was set by four hand-written copies of one `.replace(/<html…/)` and by nothing in the prerender, and adding a second attribute to that arrangement is how the next one reaches three paths out of five. There is one `applyDocumentAttrs` now, and the prerender is one of its callers — which fixes the locale as a side effect of having somewhere to put the zone.
126
+
127
+ ### Internal (no consumer-facing effect)
128
+
129
+ - **@voltro/cli** — `voltro data backup --assets` / `restore --assets` are now driven against a REAL S3 API (MinIO in the test stack), over the network, with a real backup and a real restore into a second bucket and the bytes compared.
130
+
131
+ The asset half rests on one field: a provider must map "there is no object at that key" to `status === 404`, and nothing looser — a 403 from a rotated credential is also non-transient, and calling that "the object is gone" turns a recoverable outage into a backup that quietly contains nothing. That mapping was measured against memory, filesystem and database live, and against the s3/azure SDK error SHAPES constructed. A constructed shape is a claim about an SDK, not about a round trip: nothing in it exercises signing, path-style addressing, or what the SDK actually raises when a server answers `NoSuchKey`. Two deployments listed exactly this as the gap they could not close either.
132
+
133
+ Both directions are covered against the real server: a dangling reference is stepped over and reported, and a bad credential fails the capture rather than being read as a missing object.
134
+
135
+ The native dialect lane also stops being silent about mssql. It was absent from the array entirely — an absent lane and a covered one look identical from the outside — and it is now listed with a written reason for why it does not register here (`sqlpackage` is a separate Microsoft download on a .NET runtime, absent from `mcr.microsoft.com/mssql-tools`). It is registered rather than skipped-forever, because a skip present on every healthy run teaches readers to ignore skip lines; and an assertion fails if any lane drops out WITHOUT a written reason, or if a reason names a lane that is in fact running.
136
+ - **@voltro/cli, @voltro/plugin-storage** — Coverage for the data commands, at the level the defects actually live.
137
+
138
+ `dataDirectFlags.e2e.test.ts` drives every DIRECT-target flag of `voltro data export` / `import` through the real binary against a real sqlite database, and carries the same `DATA_FLAGS`-driven self-check the native suite has: a new direct flag has to be driven there or the file goes red. The api-only flags are listed explicitly with the reason they are not here, and that list is asserted against `DATA_FLAGS` so it cannot become a place to hide an untested flag.
139
+
140
+ `missingObjectIs404.test.ts` pins the contract the dangling-reference skip rests on: every provider maps "no object at that key" to `status === 404`, and nothing looser. Five providers, three of them live, s3 and azure through their SDK's real error shapes — which read DIFFERENT fields (`$metadata.httpStatusCode` vs a bare `statusCode`), so a mapping copied from one to the other would turn dangling references back into hard capture failures on that backend alone.
141
+
142
+ `codegenFeatureTables.integration.test.ts` measures the count a report was about: `voltro codegen` must carry the tables a `*.cron.tsx` contributes, which the entity walk cannot see. The structural guards beside it were TRUE while that count was wrong. `frameworkSourceTypo.integration.test.ts` measures what catches a misspelled `_voltro_*` source given that the type deliberately does not — `voltro check` reports it as a dangling source and exits 1, with a negative control so the check is not merely flagging every framework name.
143
+
144
+ ---
145
+
146
+ ## [0.50.1] — 2026-08-24
147
+
148
+ ### Added
149
+
150
+ - **@voltro/cli** — A `0.51.0` upgrade note for the case 0.45.0's typed-`source:` note left open: a source that is COMPUTED but whose names are all known — a generic reader that takes its table from `input` or a registry. Both remedies the original offered are wrong there. There is no misspelled name to fix, and widening to `ReactivitySourceValue` (or casting) compiles instantly while taking that entire set of tables permanently out of the check the narrowing exists to provide.
151
+
152
+ The note carries the recipe that keeps the names — `as const satisfies`, with the key set recovered separately — and the trap under it: an ANNOTATION widens the keys back even when the literal carries `as const`, because it is checked against the literal and then replaces its type. Filed under an unreached version because codemod selection is `from < version <= to`: 0.45.0's note can never fire again for anyone already past it. Documented on the subscriptions page in both languages.
153
+
154
+ ### Changed
155
+
156
+ - **@voltro/data-transfer** — The `mariadb-dump`-not-on-PATH hint now also warns that a MariaDB 12.x client requires TLS by default, so running it BY HAND against a server without TLS fails with `TLS/SSL error: SSL is required` (2026) and needs `--skip-ssl`. Our own invocation is unaffected. It is said here because this hint is what sends people to install that client package, and the next thing many of them do is run the tool by hand — where the failure reads like a broken install of the thing we just told them to install.
157
+
158
+ ### Fixed
159
+
160
+ - **@voltro/cli, @voltro/mcp** — `voltro check`'s `rbac/unguarded-mutation` no longer fires on `internal: true` procedures. Those are in no rpc group and on no route, so the finding's own sentence — "any caller who can reach the rpc surface can invoke it" — named a surface that does not exist, and neither remedy it offered was available: a guard protects nothing there, and `openAccess` is refused outright on an internal descriptor. An app with 990 internal mutations got 990 warnings and a `FAILED` exit from the same tree on which `voltro doctor` reported zero procedures without an access decision, which made `check` unusable as the CI gate it exists to be.
161
+
162
+ The capability manifest now carries `internal: true` per procedure (present only when true), and the rule reads it through `isWireReachable` — the same predicate the boot access gate and all three rpc-group assemblies use, rather than a fourth spelling of the same flag. A manifest that does not carry the field is read as REACHABLE, so the rule stays loud on an api older than the field rather than going quiet on it.
163
+ - **@voltro/data-transfer, @voltro/cli** — `--assets` aborted the whole capture at the first blob reference the storage provider could not resolve. A dangling reference is a fact about the source data — a row pointing at an object that was deleted, or that never arrived because an earlier import ran without the flag — and no backup can put back bytes that are not there. Aborting made the flag unusable for exactly the deployment that needed it: 178 references, one resolvable, and the run stopped at the second.
164
+
165
+ The phase now records the key, steps over it, and the run reports how many were skipped. Only a genuine not-found (`status === 404`) is treated this way; a 403 from a rotated credential or a 5xx from a backend outage still fails the capture, because calling those "the object is gone" turns a recoverable outage into a backup that quietly contains nothing.
166
+
167
+ The same abort also cost the artefact its provenance: `voltro-backup-stamp.json` was written after the asset phase, so a backup taken WITH `--assets` had no stamp at all, and `restore` then greeted an artefact this tool had written minutes earlier as "an older/handmade backup. Cannot verify dialect or schema version." The stamp is now written on every path — it describes the dump, and the dump is on disk and correct by the time the assets run.
168
+ - **@voltro/cli** — `voltro doctor`'s eager-loaded-relation rule resolves nested relations against their PARENT's target table instead of against the query's roots, so every level is reported in one pass. It flattened the `.with({ … })` tree before resolving, which meant a level-2 relation stayed invisible until level 1's table had been added to `source:` — each fix revealed the next level, and a clean run after the first fix meant nothing. One app ran `doctor → fix → doctor` twice before the output stopped producing new findings.
169
+
170
+ The one case that is still genuinely under-reported is now stated instead of being silent: when the executor's base table is neither declared nor textually readable, nothing resolves, and doctor prints which relation names it could not check rather than reporting clean.
171
+ - **@voltro/data-transfer** — A native restore whose client exited before consuming its input took the whole CLI down with an unhandled `EPIPE`. `createReadStream(dump).pipe(child.stdin)` had no `error` listener, so Node threw on a write to a pipe with no reader:
172
+
173
+ Error: write EPIPE Emitted 'error' event on Socket instance at: at Socket.onerror (node:internal/streams/readable:1045:14)
174
+
175
+ What that cost is the point. The crash landed before `close`, so the step never resumed: the client's stderr — the one place the reason was written down — was discarded, the transfer row was never closed, the in-progress marker was never re-asserted, and the operator got a raw Node stack trace over a database that was now half replaced. A deployment met it on a 64 MB restore and could not diagnose it at all, because the only diagnosis had been thrown away.
176
+
177
+ Every stream in the step now carries a handler. `EPIPE` on the child's stdin is deliberately swallowed — a child closes the pipe by dying, and its exit code and stderr are the actual failure — while any other stream error is carried and reported. The failure line now always includes the tool's stderr (and says so explicitly when the tool printed none), plus how many bytes of the artefact had been fed before it stopped. A source error also ends the child's stdin, which turns a restore that used to hang forever on a vanished dump into one that fails, and a clean exit over a broken input stream is reported as a failure rather than as a truncated success.
178
+ - **@voltro/cli** — `voltro check`'s observed-vs-declared findings advised an op that does not exist. The recorder reports what the store did, and `store.upsert` is one call; a `target:` is `InsertTarget | UpdateTarget | DeleteTarget` and has no `upsert` member. So `observed/undeclared-write` said to add `{ table: '…', op: 'upsert' }` — a `TS2322`, from a line whose whole purpose is to be pasted. It now advises both halves, and the observed → declarable mapping is typed `Record<ObservedWriteOp, ReadonlyArray<TargetSpec['op']>>` so a new recorder op cannot ship without a declarable answer.
179
+
180
+ The remedy no longer produces a finding of its own either: declared targets are now collected per table as a SET. `new Map(targets.map(t => [t.table, t.op]))` kept only the last op, so the correct two-entry declaration read as "declares update" and the insert half came back as a `wrong-op`.
181
+ - **@voltro/data-transfer, @voltro/cli** — The in-progress restore marker did not survive a native restore, on the exact dialects that were documented as safe. The claim — postgres and the mysql family drop only the objects the dump names, so the marker row survives — had a correct argument and a wrong premise: a native dump names the WHOLE database, `_voltro_replace_in_progress` included, and a mysql-family restore writes `DROP TABLE IF EXISTS` in front of each table. The table sorts early, so the guard was removed near the START of the window it exists to cover. Measured downstream as one row before and zero after, twice, once by hand and once through the command — and then a restore died and left a database with a schema, some of its tables, no users and no blobs, with nothing to stop the next boot.
182
+
183
+ Two changes, covering different dumps. `voltro data backup` now excludes the marker table (`--exclude-table` / `--ignore-table`), so an artefact we produce cannot carry the thing that erases the guard on the way back in. And `restore` re-asserts the marker after the tool exits, on the failing path as well as the succeeding one, which covers dumps taken before this change and dumps made by hand: rewritten is a warning, and unwritable is an error, because the guard is then off for that run.
184
+ - **@voltro/plugin-storage, @voltro/cli** — `--assets` could not be refused when no storage was configured, and resolved the wrong backend when it was.
185
+
186
+ `resolveStorageProvider({})` is total: its final `default:` arm returns an in-process `memoryProvider`. So the three `if (!provider)` refusals in `voltro data backup` / `export` / `restore` — whose entire job is to refuse a flag the app cannot honour — were checking a condition that cannot hold. What they let through is worse than an unchecked flag: with no storage configured, `backup --assets` captured from a fresh memory provider, found nothing in it, and wrote an artefact stamped `assets: { count: 0 }` — a rollback story that says the blobs are in there.
187
+
188
+ It also ignored the provider the app REGISTERED. An app on `storagePlugin({ provider: s3(…) })` had its export/import/backup read and write the env-derived default instead, which surfaces later as "no object at key" — the exact drift `appStorageProvider()` exists to prevent, in call sites that never adopted it.
189
+
190
+ `configuredStorageProvider()` is the honest predicate: the app's registered provider, else the env-named one, else `undefined`. A memory provider nobody asked for is not a decision. The CLI now loads the app config BEFORE resolving — `storagePlugin(...)` registers at construction, so asking first answered with the default no matter what the app had configured.
191
+
192
+ ### Internal (no consumer-facing effect)
193
+
194
+ - **@voltro/cli** — `voltro data backup` / `restore` / `clear-replace-marker` are now driven end to end, one case per declared option and one per refusal, as real CLI subprocesses against a real database.
195
+
196
+ Every defect this command has shipped lived in the SEQUENCE rather than in a function — an unhandled `EPIPE` that discarded the client's stderr before `close` fired, a marker erased by the restore's own artefact, an asset abort that skipped the provenance stamp, a refusal that could not fire — so none of them was reachable by testing a part. `nativeBackupRestore.e2e.test.ts` covers the whole surface against sqlite (a file copy: no vendor binary, runs anywhere) and `nativeBackupRestoreDialects.integration.test.ts` runs the vendor-tool half against postgres, mariadb and mysql, skipping BY NAME when the server or the client binary is absent — including when the client is present but too old for the server, which a "is it on PATH" check reports as ready.
197
+
198
+ The suite checks itself against `DATA_FLAGS`, the command's own declared option list, so an option cannot be added without a case that drives it.
199
+
200
+ ---
201
+
42
202
  ## [0.50.0] — 2026-08-23
43
203
 
44
204
  ### ⚠ BREAKING
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-atlassian",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "Jira + Confluence plugin — JiraService + ConfluenceService over the Atlassian REST/Greenhopper/Agile APIs, with a pluggable per-subject credentials resolver (PAT), transient retry + Retry-After, timeouts, an SSRF-guarded PAT-free avatar proxy, and optional response caching via @voltro/cache.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -43,8 +43,8 @@
43
43
  "node": ">=24.0.0"
44
44
  },
45
45
  "dependencies": {
46
- "@voltro/integration-http": "0.50.0",
47
- "@voltro/protocol": "0.50.0"
46
+ "@voltro/integration-http": "0.51.0",
47
+ "@voltro/protocol": "0.51.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "effect": "^3.22.0"