From 08c986507aeb3d74e801f7ecaa38c48d6168095f Mon Sep 17 00:00:00 2001 From: TonyTonyCoder11 Date: Fri, 31 Jul 2026 17:36:05 +0200 Subject: [PATCH 1/2] Punctuate the documentation the way the sentences read Twenty-two em dashes across the README, STABILITY.md, the migration guide, CONTRIBUTING.md and the 2.0.0 changelog section, replaced with whatever the sentence actually wanted: a colon before a list, a full stop between two independent clauses, commas around a real aside. Several of them were standing in for two different marks in the same sentence, which is where the habit stops being shorthand and starts costing the reader. The one that stays is the status blockquote, whose shape STANDARD.md prescribes and the audit script checks. Mechanical boldface goes with it. STABILITY.md had nine paragraphs opening with a bolded sentence acting as a heading, and when every paragraph carries an emphasised line none of them is emphasised. Nineteen down to nine; what remains marks MAJOR, MINOR and PATCH, where the bold separates a defined term from its definition. Changelog sections older than 2.0.0 keep their punctuation. Their text generated the GitHub Release bodies at the time, and editing them now would leave the two disagreeing about a release nobody can cut again. --- CHANGELOG.md | 12 +++++----- CONTRIBUTING.md | 2 +- README.md | 22 ++++++++--------- STABILITY.md | 35 ++++++++++++++-------------- docs/migrating-from-qdrant-client.md | 10 ++++---- 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb9d1f2..b95c719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Tier 5, complete, and the release the transport seam was built for. `kdrant-tran opt-in gRPC engine behind the same `QdrantClient`, and `kdrant-core` compiles for the JVM and eight Kotlin/Native targets. Adding a second engine changed no line of `kdrant-core`. -**Two things make this a major.** `kdrant-core`'s JVM classes moved to `kdrant-core-jvm`, because the +Two things make this a major. `kdrant-core`'s JVM classes moved to `kdrant-core-jvm`, because the module is multiplatform now: a Gradle build changes only the version number, a Maven build naming `kdrant-core` has to move. And `ScrollRequest` and `SearchRequest` gained a `shardKey` parameter, which changed their generated constructor and `copy`, so code that called `copy()` on either against a `1.x` @@ -63,9 +63,9 @@ API at all. See [STABILITY.md](STABILITY.md#upgrading-from-1-x). client. Nothing changes for a REST user: the module is separate, and a build that does not ask for it resolves no gRPC, no protobuf and no Netty. The stubs are generated from Qdrant's own `.proto` files, vendored verbatim at v1.18.2, rather than - taken from `io.qdrant:client` — grpc-kotlin emits suspend functions and `Flow`s, which is the shape - the transport seam already has, and generating decides the dependency set instead of inheriting a - shaded Netty jar that is most of the official client's footprint. + taken from `io.qdrant:client`. grpc-kotlin emits suspend functions and `Flow`s, which is the shape the + transport seam already has, and generating decides the dependency set instead of inheriting a shaded + Netty jar that is most of the official client's footprint. - Both engines are held to one **shared client contract** (M31, `kdrant-testkit`), which runs the same 30 behavioural tests against a real Qdrant over each protocol. The REST tests that came before it asserted HTTP bodies, which a gRPC engine cannot satisfy by construction. @@ -74,7 +74,7 @@ API at all. See [STABILITY.md](STABILITY.md#upgrading-from-1-x). `linuxArm64`, `linuxX64` and `mingwX64`. Kotlin/JS is deliberately not among them: there is no JS engine, so the target would ship models with nothing to send them over, and its test tooling is the only npm dependency graph this repository would have. The models, DSLs, error hierarchy and client - logic were already free of the JVM — that is what the transport seam was for — so the migration moved + logic were already free of the JVM, which is what the transport seam was for, so the migration moved sources into `commonMain` and changed one declaration. The engines stay JVM-only, because Ktor CIO and grpc-java are. - A `commonTest` suite that runs on every target, covering the places a platform could actually @@ -101,7 +101,7 @@ API at all. See [STABILITY.md](STABILITY.md#upgrading-from-1-x). REST API and Qdrant serves these over HTTP only: `telemetry`, `metrics`, `listIssues`, `clearIssues`, `recoverSnapshot`, snapshot download and upload, and the five shard-scope snapshot operations. On the gRPC engine each throws an `UnsupportedOperationException` naming the operation and pointing at REST, - rather than degrading quietly — a snapshot download that returns nothing is a backup that does not + rather than degrading quietly. A snapshot download that returns nothing is a backup that does not exist. The REST engine is unchanged. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index efe36ae..0303085 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Thanks for your interest in contributing. - Keep changes focused and covered by tests. - Follow the existing style (`.editorconfig`: 4-space indentation, 120-column lines, no wildcard imports). -- If you change the public API, run `./gradlew apiDump` and commit the updated `*.api` files — CI +- If you change the public API, run `./gradlew apiDump` and commit the updated `*.api` files. CI runs `apiCheck` and will fail on an untracked API change. - Add an entry under `[Unreleased]` in `CHANGELOG.md`. diff --git a/README.md b/README.md index d9d7a57..423ab23 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [![Website](https://img.shields.io/badge/website-nacodestudios.it-232B45?labelColor=0B0E17)](https://nacodestudios.it/en/project/kdrant) Qdrant's official JVM client is built for Java: every call returns a `ListenableFuture`, requests are -assembled with protobuf builders, and the wire is decided for you — gRPC, with a shaded Netty on your +assembled with protobuf builders, and the wire is decided for you: gRPC, with a shaded Netty on your classpath whether you needed the throughput or not. Kdrant is the client you'd actually want to write Kotlin against: `suspend` functions, a type-safe DSL, `kotlinx-serialization` models, and a wire you pick. The default engine is pure-Kotlin REST and pulls in no gRPC, no protobuf and no Netty; the gRPC @@ -135,9 +135,9 @@ val qdrant: QdrantClient = Every example below reads the same either way, because the API above the wire is the same API. Two differences are worth knowing before you switch. The **port** is 6334, not 6333, and nothing rewrites -it for you. And Qdrant serves eleven operations over HTTP only — telemetry, Prometheus metrics, the -issues endpoint, snapshot recovery, snapshot download and upload, and the shard-scope snapshots — which -the gRPC engine refuses by name rather than degrading quietly. +it for you. And Qdrant serves eleven operations over HTTP only: telemetry, Prometheus metrics, the +issues endpoint, snapshot recovery, snapshot download and upload, and the shard-scope snapshots. The +gRPC engine refuses each of them by name rather than degrading quietly. `kdrant-core` is a Kotlin Multiplatform library and publishes one artifact per target. A Gradle build resolves the right one from the `kdrant-core` coordinate and needs no change. A Maven build names the @@ -337,10 +337,10 @@ kdrant-core QdrantClient, models, DSLs, KdrantException, QdrantTranspor +-- kdrant-transport-grpc grpc-kotlin KdrantGrpc(host) JVM ``` -That is the arrangement the second engine tested. Adding gRPC changed **no line of `kdrant-core`**, and -the same behavioural suite runs against both engines against the same Qdrant, so the choice between them -is a footprint and throughput decision rather than a feature one — except for the operations Qdrant -serves over HTTP only, which the gRPC engine names. +That is the arrangement the second engine tested. Adding gRPC changed no line of `kdrant-core`, and the +same behavioural suite runs against both engines against the same Qdrant, so the choice between them is +a footprint and throughput decision rather than a feature one. The exception is the set of operations +Qdrant serves over HTTP only, which the gRPC engine names. It is also why the core compiles for iOS, macOS, Linux and Windows: code that never knew there was a wire has nothing platform-specific to port. The engines stay JVM-only, because Ktor CIO and grpc-java @@ -353,7 +353,7 @@ the target would ship a DSL with nothing to send. **Shipped (`2.0.0`).** Tier 5 closes the two things the transport seam existed to make possible. `kdrant-transport-grpc` is an opt-in gRPC engine behind the same `QdrantClient`, generated from Qdrant's own `.proto` files rather than wrapping the official client, so a REST build still resolves no gRPC, no -protobuf and no Netty — checked on every build, not asserted. And `kdrant-core` moved to Kotlin +protobuf and no Netty, checked on every build rather than asserted. And `kdrant-core` moved to Kotlin Multiplatform: the JVM plus eight Kotlin/Native targets, a migration that changed no public API at all. Both engines are now held to one shared behavioural suite against a real Qdrant. The major bump is for the artifact layout, not the API: `kdrant-core`'s JVM classes moved to `kdrant-core-jvm`, which a @@ -371,8 +371,8 @@ engine, and typed-payload DX. The [CHANGELOG](CHANGELOG.md) has the version-by-v **Next.** Nothing is claimed yet; the board is where it gets decided. -The plan lives on the [Kdrant board](https://github.com/orgs/NaCode-Studios/projects/4) — one item per milestone, each with its -exit criterion — and every tier is a [milestone](https://github.com/NaCode-Studios/Kdrant/milestones) in this repository. See +The plan lives on the [Kdrant board](https://github.com/orgs/NaCode-Studios/projects/4), one item per milestone, each with its +exit criterion. Every tier is a [milestone](https://github.com/NaCode-Studios/Kdrant/milestones) in this repository. See [STABILITY.md](STABILITY.md) for the versioning and stability policy. ## Building and testing diff --git a/STABILITY.md b/STABILITY.md index bcec11e..0b3f253 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -1,6 +1,6 @@ # Stability & versioning -This document is the written stability contract for Kdrant — what "stable" means, what changes are +This document is the written stability contract for Kdrant: what "stable" means, what changes are allowed in which releases, and what a major bump costs you. It complements the [board](https://github.com/orgs/NaCode-Studios/projects/4) (where the project is going) and the [CHANGELOG](CHANGELOG.md) (what has already shipped). @@ -8,9 +8,9 @@ project is going) and the [CHANGELOG](CHANGELOG.md) (what has already shipped). Kdrant follows [Semantic Versioning 2.0.0](https://semver.org). Given `MAJOR.MINOR.PATCH`: -- **MAJOR** — incompatible public-API changes. -- **MINOR** — backwards-compatible additions (new operations, new optional parameters, new overloads). -- **PATCH** — backwards-compatible bug fixes. +- **MAJOR**: incompatible public-API changes. +- **MINOR**: backwards-compatible additions (new operations, new optional parameters, new overloads). +- **PATCH**: backwards-compatible bug fixes. ### What counts as "public API" @@ -18,8 +18,9 @@ The public API is exactly what the [binary-compatibility-validator](https://gith tracks in the committed `*.api` files (`kdrant-core/api`, both transport engines, and the ecosystem modules; the gRPC module's generated protobuf and stub classes are excluded, because their surface is Qdrant's to change and not ours to promise). Every public/protected symbol is in those dumps; `./gradlew apiCheck` fails the build on any -untracked change, so **API breakage is never silent**. Anything not in the `*.api` files — `internal` -declarations, or symbols annotated `@InternalKdrantApi` — is not public API and may change at any time. +untracked change, so API breakage is never silent. Anything not in the `*.api` files, meaning +`internal` declarations and symbols annotated `@InternalKdrantApi`, is not public API and may change at +any time. The **wire behaviour** of an engine (the requests it sends and the responses it parses) is also part of the contract: a change that alters what goes on the wire for an existing operation is treated as a @@ -28,8 +29,8 @@ tests validate every request body the REST engine builds against Qdrant's own Op to the version the CI matrix runs against, and a shared behavioural suite runs both engines against a real Qdrant, so a wire change is a failing build rather than a silent difference. -**The two engines are held to the same behaviour, with one stated exception.** Qdrant serves eleven -operations over HTTP only — telemetry, Prometheus metrics, the issues endpoint, `recoverSnapshot`, +The two engines are held to the same behaviour, with one stated exception. Qdrant serves eleven +operations over HTTP only: telemetry, Prometheus metrics, the issues endpoint, `recoverSnapshot`, snapshot download and upload, and the five shard-scope snapshot operations. On the gRPC engine each throws an `UnsupportedOperationException` naming itself and naming REST. That list is part of this contract: an operation leaving it is an additive change, and an operation joining it would be a @@ -40,14 +41,14 @@ breaking one. Two Kotlin details are additive to `apiCheck` but not binary-compatible for every caller, and it is better to say so than to discover it later. -**`QdrantClient` and `QdrantTransport` are interfaces you call, not interfaces you implement.** Kdrant +`QdrantClient` and `QdrantTransport` are interfaces you call, not interfaces you implement. Kdrant adds operations to both as Qdrant grows, and a new member on a Kotlin interface breaks a class that implemented it against an older jar. If you need a decorator, delegate to the interface (`by transport`) so the compiler tells you what a new release added; if you need a stub in a test, generate it. A third-party wire engine is a supported use of `QdrantTransport`, but it is a use that recompiles against each minor. -**Adding a field to a public `data class` changes its generated `copy` and `componentN`.** New response +Adding a field to a public `data class` changes its generated `copy` and `componentN`. New response fields arrive as Qdrant returns more, and while the constructor keeps its defaults and source keeps compiling, code that called `copy()` against an older jar needs recompiling. Kdrant does not add fields gratuitously and each one is listed in the [CHANGELOG](CHANGELOG.md), but a minor upgrade is a @@ -68,22 +69,22 @@ Within a major version: ## Upgrading from `1.x` -`2.0.0` breaks two things, and **source compatibility is not one of them**: every `1.x` call site -compiles unchanged. +`2.0.0` breaks two things, and source compatibility is not one of them: every `1.x` call site compiles +unchanged. -**Where `kdrant-core`'s JVM classes are published.** The module is Kotlin Multiplatform now, so its own +The first is where `kdrant-core`'s JVM classes are published. The module is Kotlin Multiplatform now, so its own coordinate carries Gradle module metadata and the classes live in `kdrant-core-jvm`. Gradle reads that metadata and resolves the variant, so a Gradle build changes nothing but the version number. Maven does not read it, so a Maven build naming `kdrant-core` gets no classes and must move to `kdrant-core-jvm`. Depending on `kdrant-transport-rest` or `kdrant-transport-grpc`, which is what the README recommends, is unaffected either way. -**`ScrollRequest` and `SearchRequest` gained a `shardKey` parameter.** That is the case +The second is that `ScrollRequest` and `SearchRequest` gained a `shardKey` parameter. That is the case [above](#what-may-still-change-in-a-minor) arriving for real: the constructor keeps its defaults and source keeps compiling, but the generated `copy` and `componentN` changed, so code that called `copy()` on either type against a `1.x` jar has to be recompiled. Nothing else in the `*.api` dump was removed. -**The multiplatform migration itself changed no public API.** The dump is identical either side of it, +The multiplatform migration itself changed no public API. The dump is identical either side of it, which is worth stating because it is the part that sounds like it should have broken something. What broke is the two lines above. @@ -92,10 +93,10 @@ formula and MMR reranking, shard-scope snapshots and the Koog module are new ope ## Java interoperability -Kdrant is deliberately **Kotlin-coroutine-first** — that is the wedge (see the [README](README.md)). The +Kdrant is deliberately Kotlin-coroutine-first, which is the wedge (see the [README](README.md)). The public API is `suspend` functions and `Flow`s, which are callable from Java but not idiomatic there. -**No bundled `CompletableFuture` facade.** Mirroring ~40 suspend operations into a +There is no bundled `CompletableFuture` facade. Mirroring ~40 suspend operations into a blocking or future-returning Java API is a large, duplicated surface to maintain, and it is not the audience Kdrant optimises for. Java callers who need it should bridge with the standard tools: diff --git a/docs/migrating-from-qdrant-client.md b/docs/migrating-from-qdrant-client.md index 41cc187..c63eba3 100644 --- a/docs/migrating-from-qdrant-client.md +++ b/docs/migrating-from-qdrant-client.md @@ -16,9 +16,9 @@ same API. What that engine cannot do is listed under [What this is not](#what-th ## What this is not -The gRPC engine is not the whole of Qdrant. Eleven operations are served over HTTP only — telemetry, +The gRPC engine is not the whole of Qdrant. Eleven operations are served over HTTP only: telemetry, Prometheus metrics, the issues endpoint, snapshot recovery, snapshot download and upload, and the -shard-scope snapshots — and on the gRPC engine each throws rather than pretending. If you need any of +shard-scope snapshots. On the gRPC engine each of them throws rather than pretending. If you need any of them, use the REST engine for the whole client or for that one call. The footprint trade-off between the two is set out in the [README](../README.md#footprint-vs-the-official-client). @@ -41,13 +41,13 @@ dependencies { ## Creating a client ```java -// io.qdrant:client — gRPC, port 6334 +// io.qdrant:client, gRPC on port 6334 QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder("localhost").withApiKey("").build()); ``` ```kotlin -// Kdrant — REST, port 6333 +// Kdrant, REST on port 6333 val qdrant = Kdrant(host = "localhost", port = 6333) { apiKey = "" useTls = true @@ -204,7 +204,7 @@ backoff, honouring `Retry-After`; what surfaces is the failure that survived the | `io.qdrant:client` | Kdrant | | --- | --- | | `createCollectionAsync(name, params)` | `createCollection(name) { vector { … } }` | -| — | `ensureCollection(name) { vector { … } }` | +| no equivalent | `ensureCollection(name) { vector { … } }` | | `deleteCollectionAsync(name)` | `deleteCollection(name)` | | `collectionExistsAsync(name)` | `collectionExists(name)` | | `getCollectionInfoAsync(name)` | `getCollection(name)` | From 5ed3507e0b5debf7bf283cfe92b4d75e3b499f40 Mon Sep 17 00:00:00 2001 From: TonyTonyCoder11 Date: Fri, 31 Jul 2026 17:39:52 +0200 Subject: [PATCH 2/2] Count the operations gRPC cannot serve Fourteen, not eleven. The number came from the KDoc on GrpcQdrantTransport, where it was written from memory and never checked against the class, and from there it reached the README, STABILITY.md, the migration guide and the changelog. Two mistakes made it wrong. The enumeration said "the five shard-scope snapshot operations" and there are six, since recoverShardSnapshot was missing from the list. And downloadStorageSnapshot was left out entirely. Counting the call sites settles it: grep -o 'restOnly("[a-zA-Z]*")' GrpcQdrantTransport.kt | sort -u | wc -l 14 The enumeration is now written the way the code groups them, so the next person can count the same fourteen without reconstructing the list: the two issues calls together, the snapshot and storage-snapshot transfers together, and the six shard-scope operations as a group. --- CHANGELOG.md | 7 ++++--- README.md | 7 ++++--- STABILITY.md | 6 +++--- docs/migrating-from-qdrant-client.md | 7 ++++--- .../dev/kdrant/transport/grpc/GrpcQdrantTransport.kt | 5 +++-- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b95c719..49f928d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,9 +97,10 @@ API at all. See [STABILITY.md](STABILITY.md#upgrading-from-1-x). - `kdrant-core`'s `-javadoc.jar` holds Dokka's HTML output rather than Javadoc HTML: the Dokka Javadoc generator refuses a multiplatform project. Maven Central requires the jar to exist rather than to be Javadoc, and HTML is what a Kotlin reader wants. -- Eleven `QdrantTransport` operations have no gRPC equivalent, because the seam was shaped by Qdrant's - REST API and Qdrant serves these over HTTP only: `telemetry`, `metrics`, `listIssues`, `clearIssues`, - `recoverSnapshot`, snapshot download and upload, and the five shard-scope snapshot operations. On the +- Fourteen `QdrantTransport` operations have no gRPC equivalent, because the seam was shaped by + Qdrant's REST API and Qdrant serves these over HTTP only: `telemetry`, `metrics`, `listIssues`, + `clearIssues`, `recoverSnapshot`, the snapshot and storage-snapshot transfers, and the six + shard-scope snapshot operations. On the gRPC engine each throws an `UnsupportedOperationException` naming the operation and pointing at REST, rather than degrading quietly. A snapshot download that returns nothing is a backup that does not exist. The REST engine is unchanged. diff --git a/README.md b/README.md index 423ab23..b28a0dd 100644 --- a/README.md +++ b/README.md @@ -135,9 +135,10 @@ val qdrant: QdrantClient = Every example below reads the same either way, because the API above the wire is the same API. Two differences are worth knowing before you switch. The **port** is 6334, not 6333, and nothing rewrites -it for you. And Qdrant serves eleven operations over HTTP only: telemetry, Prometheus metrics, the -issues endpoint, snapshot recovery, snapshot download and upload, and the shard-scope snapshots. The -gRPC engine refuses each of them by name rather than degrading quietly. +it for you. And Qdrant serves fourteen operations over HTTP only: telemetry, Prometheus metrics, the +two issues calls, snapshot recovery, the snapshot and storage-snapshot transfers, and the six +shard-scope snapshot operations. The gRPC engine refuses each of them by name rather than degrading +quietly. `kdrant-core` is a Kotlin Multiplatform library and publishes one artifact per target. A Gradle build resolves the right one from the `kdrant-core` coordinate and needs no change. A Maven build names the diff --git a/STABILITY.md b/STABILITY.md index 0b3f253..1ebca6f 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -29,9 +29,9 @@ tests validate every request body the REST engine builds against Qdrant's own Op to the version the CI matrix runs against, and a shared behavioural suite runs both engines against a real Qdrant, so a wire change is a failing build rather than a silent difference. -The two engines are held to the same behaviour, with one stated exception. Qdrant serves eleven -operations over HTTP only: telemetry, Prometheus metrics, the issues endpoint, `recoverSnapshot`, -snapshot download and upload, and the five shard-scope snapshot operations. On the gRPC engine each +The two engines are held to the same behaviour, with one stated exception. Qdrant serves fourteen +operations over HTTP only: telemetry, Prometheus metrics, the two issues calls, `recoverSnapshot`, the +snapshot and storage-snapshot transfers, and the six shard-scope snapshot operations. On the gRPC engine each throws an `UnsupportedOperationException` naming itself and naming REST. That list is part of this contract: an operation leaving it is an additive change, and an operation joining it would be a breaking one. diff --git a/docs/migrating-from-qdrant-client.md b/docs/migrating-from-qdrant-client.md index c63eba3..b2ffc58 100644 --- a/docs/migrating-from-qdrant-client.md +++ b/docs/migrating-from-qdrant-client.md @@ -16,9 +16,10 @@ same API. What that engine cannot do is listed under [What this is not](#what-th ## What this is not -The gRPC engine is not the whole of Qdrant. Eleven operations are served over HTTP only: telemetry, -Prometheus metrics, the issues endpoint, snapshot recovery, snapshot download and upload, and the -shard-scope snapshots. On the gRPC engine each of them throws rather than pretending. If you need any of +The gRPC engine is not the whole of Qdrant. Fourteen operations are served over HTTP only: telemetry, +Prometheus metrics, the two issues calls, snapshot recovery, the snapshot and storage-snapshot +transfers, and the six shard-scope snapshot operations. On the gRPC engine each of them throws rather +than pretending. If you need any of them, use the REST engine for the whole client or for that one call. The footprint trade-off between the two is set out in the [README](../README.md#footprint-vs-the-official-client). diff --git a/kdrant-transport-grpc/src/main/kotlin/dev/kdrant/transport/grpc/GrpcQdrantTransport.kt b/kdrant-transport-grpc/src/main/kotlin/dev/kdrant/transport/grpc/GrpcQdrantTransport.kt index 1e0e137..167e93a 100644 --- a/kdrant-transport-grpc/src/main/kotlin/dev/kdrant/transport/grpc/GrpcQdrantTransport.kt +++ b/kdrant-transport-grpc/src/main/kotlin/dev/kdrant/transport/grpc/GrpcQdrantTransport.kt @@ -66,8 +66,9 @@ import kotlin.random.Random * services. * * **The seam is wider than the protocol.** `QdrantTransport` was shaped by Qdrant's REST API, and - * eleven of its operations have no gRPC equivalent: telemetry, Prometheus metrics, the issues - * endpoint, snapshot recovery, snapshot download and upload, and the whole shard-scope snapshot group. + * fourteen of its operations have no gRPC equivalent: telemetry, Prometheus metrics, the two issues + * calls, snapshot recovery, the snapshot and storage-snapshot transfers, and the six shard-scope + * snapshot operations. * They are not silently degraded here. Each throws an [UnsupportedOperationException] naming the * operation and pointing at the REST engine, because a snapshot download that quietly returns nothing * is a backup that quietly does not exist. A `KdrantException` would have been the wrong type: nothing