The registry owns the set of available strategies and keeps the engine decoupled from any
concrete protocol. `createStrategyRegistry()` returns an **empty** registry — you populate it
with `.set()`.

## Why it exists

* The core never imports strategy implementations.
* Strategies can be swapped or added without changing engine logic.
* Registry key types come from your intent map `M`, so the backend's intent response and the
  registered strategies are kept in sync at compile time.

## API

`Contracts.Strategy.Registry<M, C, P, R>`:

```ts
type Registry<M, C, P, R> = {
  get<K>(id: K): Contracts.Strategy.Me<M, C, P, R, K> | undefined
  has(id: string): id is keyof M & string
  set<K>(strategy: Contracts.Strategy.Me<M, C, P, R, K>): void
}
```

## Usage

```ts
import { PostStrategy, multipartStrategy, createStrategyRegistry } from '@gentleduck/upload/strategies'

const strategies = createStrategyRegistry<Intents, Cursors, Purpose, Result>()
strategies.set(PostStrategy())
strategies.set(multipartStrategy())
```

There is no array constructor form — always create empty and `set()` each strategy.

## How resolution works

When your backend returns an intent, the engine reads its `strategy` field and calls
`registry.get(strategy)`. A hit runs that strategy's `start()`. A miss moves the item to the
`error` phase with a `strategy_missing` error naming the unregistered id.

## Writing a custom strategy

A strategy is a plain object with `id`, `resumable`, and `start`. The context gives you the
file, the intent, the injected transport, your API, progress reporting, and cursor helpers.

```ts
import type { Contracts } from '@gentleduck/upload/core'

type MyIntent = Contracts.Intent.Base<'my-put'> & { uploadUrl: string; token: string }
type MyCursor = { bytesUploaded: number }

function myPutStrategy<
  M extends Contracts.Intent.Map & { 'my-put': MyIntent },
  C extends Contracts.Cursor.Map<M> & { 'my-put'?: MyCursor },
  P extends string,
  R extends Contracts.Result.Base,
>(): Contracts.Strategy.Me<M, C, P, R, 'my-put'> {
  return {
    id: 'my-put',      // must equal the intent's `strategy`
    resumable: true,
    async start(ctx) {
      const offset = ctx.readCursor()?.bytesUploaded ?? 0
      await ctx.transport.put({
        url: ctx.intent.uploadUrl,
        body: ctx.file.slice(offset),
        headers: { authorization: `Bearer ${ctx.intent.token}` },
        signal: ctx.signal,
        onProgress: (uploaded) =>
          ctx.reportProgress({ uploadedBytes: offset + uploaded, totalBytes: ctx.file.size }),
      })
      ctx.persistCursor({ bytesUploaded: ctx.file.size } as C['my-put'])
    },
  }
}
```

Register it and have the backend return `{ strategy: 'my-put', ... }`:

```ts
strategies.set(myPutStrategy())
```

The `id` must exactly match the intent's `strategy` field, and both must be keys of your intent
map — the registry's generics enforce it.