# Yank and deprecate

Two operations that sound similar and are not.

| | Effect on resolution | Reversible | Deletes anything |
|---|---|---|---|
| **Yank** | stops being chosen | yes | no |
| **Deprecate** | none | yes | no |
| **Unpublish** | version is gone | no | yes |

**Yank** a version nobody should get by accident: a bad build, a wrong commit,
a release with a secret in it. **Deprecate** a version you would still rather
people did not pick, but which works.

Neither deletes anything. `unpublish`, with its 72-hour window, remains the
only destructive operation.

## Yank

A yanked version:

- is **not deleted**, and unyanking restores it;
- **stays downloadable by exact version**, so lockfiles that already pin it
  keep resolving and existing builds do not break;
- is **flagged** wherever the protocol has a place for the flag, and **excluded**
  where it does not;
- **never answers as latest**.

```sh
packr-cli yank   cargo/serde@1.0.0 --reason "built from the wrong commit"
packr-cli unyank cargo/serde@1.0.0
```

Ecosystem-agnostic API:

```
POST /api/v1/admin/packages/yank
{"ecosystem":"cargo","name":"serde","version":"1.0.0","yanked":true,"reason":"…"}
```

Cargo also has native verbs, which the registry implements:
`DELETE /cargo/api/v1/crates/{crate}/{version}/yank` and `PUT .../unyank`.

### How each ecosystem shows it

| Ecosystem | Representation |
|---|---|
| cargo | `yanked: true` in the sparse index |
| PyPI | PEP 592 `data-yanked` / `"yanked"` in the simple index |
| JVM | `yanked` on the index line |
| NuGet | `listed: false` — the protocol's own word for it |
| Go | absent from `@v/list`; `.info`, `.mod` and `.zip` keep serving so `go.sum` still verifies |
| Maven | absent from `<versions>` in `maven-metadata.xml`; the jar and pom keep serving |
| npm | excluded from `latest` resolution |
| Dart | `retracted: true` on the version entry; still downloadable by exact pin |
| Zig | absent from the version index; the pinned tarball URL keeps serving |
| OCI | absent from `GET /v2/{name}/tags/list`; the manifest and blobs keep serving by tag or digest |
| generic | absent from the version listing; the per-version files keep serving |
| gems | absent from the compact index; the pinned `<name>-<version>.gem` keeps serving |
| Swift | absent from the release map; the metadata and archive URLs keep serving by pin |
| Conan | absent from `/v2/conans/search` **and excluded from `…/{name}/*/_/_/latest` resolution** — the load-bearing half, because nothing in a range install reads the search results; the exact `…/{name}/{version}/_/_/latest` still resolves |
| Lua / LuaRocks | absent from `manifest-<luaVersion>`; the rock keeps serving by exact pin |

Five of these — Swift, Lua, gems, Go and Maven — have **no yank field in their
protocol at all**, so absence from the listing is not a fallback, it is the only
representation available. The rocks-server case is the sharpest of them: the
manifest is a Lua source file whose type checker *rejects* unknown top-level
keys, so a yank flag could not be added to it even if the client understood one.

Conan is the one row that is easy to get *half* right, and the half that looks
right is the useless one. There is no yank field either, so a yanked recipe
leaves the search index — and that part genuinely works, because an exact pin
never consults search (measured: a search returning `[]` still installed). But
**a version range resolves server-side at `…/{name}/*/_/_/latest`, and nothing
in a range install reads the search results.** So excluding a version from
search does not yank it: anyone who writes `zlib/*` still gets it. Conan's yank
is therefore two conditions, and only the second one is load-bearing.

`TestEachProtocolRendersYankItsOwnWay` in `internal/conformance/yank_test.go`
checks eleven of these rows against the adapters rather than trusting this table —
cargo, PyPI, NuGet, JVM, Go, Maven, gems, Swift, Dart and Lua. The npm, Zig,
OCI and generic rows are **not** covered by that test; they are here on the same
reasoning as the rest, which is to say they are unverified.

For those five, absence from the listing is the *only* representation, which is
why the exact-pin routes deliberately keep working.

## Deprecate

A deprecated version still resolves, still answers as latest if it is newest,
and still installs. The only effect is that clients whose protocol has
somewhere to put the message show it.

```sh
packr-cli deprecate   pypi/widget@1.0.0 --message "use widget2"
packr-cli undeprecate pypi/widget@1.0.0
```

```
POST /api/v1/admin/packages/deprecate
{"ecosystem":"pypi","name":"widget","version":"1.0.0","message":"use widget2"}
```

An empty `message` clears the notice, matching `npm deprecate ""`. A *missing*
`message` field is a 400 rather than a clear, so a malformed request cannot
silently undo a publisher's warning.

npm's own `PUT /-/v1/deprecate` still works, but only reaches npm packages.

### Where it is rendered

| Ecosystem | Representation |
|---|---|
| npm | `deprecated` on the version, in the packument and the version manifest |
| NuGet | `catalogEntry.deprecation`, the protocol's own model |
| JVM | `deprecated` on the index line |
| generic | `deprecated` on the version entry in the version listing (`internal/generic/generic.go:218`) |
| cargo, PyPI, Maven, Go, OCI | **not rendered** |

The last row is deliberate. The cargo sparse index, PEP 503/691 and
`maven-metadata.xml` define no deprecation field, and Go expresses deprecation
as a `// Deprecated:` comment inside `go.mod` — which the registry must not
rewrite, because that changes the file's hash and breaks every `go.sum` that
already records it. Inventing a non-standard field those clients ignore would
look like support without being it. The notice is still stored and still
readable through the API for the seven ecosystems listed above.

> **Nine of sixteen.** The registry has **sixteen** adapters, all **sixteen**
> enabled in production (`ENABLED_ECOSYSTEMS` in `blueforge.config.json`: npm,
> go, cargo, jvm, maven, pypi, nuget, gem, composer, dart, zig, swift, oci,
> generic, lua, conan). Of the sixteen, **gems,
> composer, zig, dart, swift, lua and oci have no deprecation rendering at all**
> — grep those packages for `Deprecat` and you get nothing. The tables above
> cover the eight that do. An earlier revision of this line said "all seven"
> without noting that the other four were simply absent, which reads as
> coverage rather than a gap.
>
> Gems is the awkward one: a yanked gem *is* withdrawn from the compact index
> (`internal/gems/index.go:35`), so yank half-works by a different mechanism,
> but the deprecation *metadata* surface npm and Maven have does not exist
> there. Composer is a GET-only mirror (`internal/composer/composer.go:96-98`)
> and has no publish path to deprecate in the first place. Dart and Zig do
> publish, and both render **yank** in their own protocol's terms — Dart as
> `retracted:true` on the version entry, still downloadable by pin, and Zig
> through the version index — so the deprecation half is the only thing
> missing, not the lifecycle. OCI is the same shape one ecosystem over: tags
> leave `/v2/{name}/tags/list` when yanked (`internal/oci/manifests.go:199`)
> while the manifest keeps serving by tag or digest, but the OCI spec has no
> deprecation field to write, and a client that ignored an invented one would
> be no better informed than before. Swift is the same story: SE-0292 has no
> yank field either, so a yanked version leaves the release map while its
> metadata and archive URLs keep serving (`internal/swift/read.go:94-97`).
>
> The Conan row shipped with the adapter (`internal/conan/`); it was previously
> listed as spec-only, from `docs/spec-2026-10-conan.md` §MEASURED §9, when the
> row described behaviour to build rather than shipped code. Its two halves are
> pinned separately: search-absence in the table above, range-exclusion in
> `TestConanRangeExcludesYanked`, because nothing in a range install reads
> search.
>
> **Yank covers every publishable ecosystem; deprecation covers eight of
> sixteen.** Fifteen of the sixteen have somewhere to represent a yank,
> and the sixteenth is Composer, which is a GET-only mirror with no publish
> path to yank in the first place. The two operations share a database
> column, which is why it is easy to assume the coverage matches — it does
> not. The two operations share a database
> column, which is why it is easy to assume the coverage matches — it does
> not.
