# `Lei.PackageRepository`
[🔗](https://github.com/kitplummer/lowendinsight/blob/main/lib/lei/package_repository.ex#L1)

Resolves a package coordinate to the repository to analyse.

A batch request names packages (`ecosystem`, `package`, `version`); an
analysis needs a repository URL. Each scanner already knew how to find one
for its registry, but only inside a function that immediately analysed it,
so a batch job could not ask the question on its own (ADR-004).

Returns `{:ok, url}` or `{:error, reason}`, never a guess: an ecosystem this
does not know is refused rather than resolved by pattern.

# `dependencies`

```elixir
@spec dependencies(String.t(), String.t(), keyword()) ::
  {:ok, [String.t()]} | {:error, term()}
```

The packages this one declares a dependency on, from the same registry
response `resolve/3` already fetches (#263).

Blast radius is not health: a dead dependency in a test helper and a dead
dependency on the request path score identically by metric counts, and only
the second is worth waking up for. In-degree within a customer's own manifest
is a proxy for that, and it needs these edges.

The request carries none — `valid_dependency?/1` accepts only ecosystem,
package and version — so the graph has to come from somewhere. It comes from
a field we were already receiving and discarding, which is the same shape as
`github_trending` reading `size` and throwing away `pushed_at`.

`{:ok, [names]}` or `{:error, reason}`. An ecosystem whose registry shape is
not handled returns `{:error, :unsupported}` rather than an empty list:
"declares nothing" and "we did not look" must not read alike, or a package
whose edges we cannot see appears to have none and sorts as peripheral.

# `describe`

```elixir
@spec describe(String.t(), String.t(), keyword()) ::
  {:ok, %{repository: String.t() | nil, dependencies: [String.t()] | nil}}
  | {:error, term()}
```

The repository URL and the declared dependencies, from **one** fetch.

`resolve/3` and `dependencies/3` each request the same registry document.
Calling both costs two round trips per package, which for a 400-package
manifest is 800 requests where 400 would do (#263).

Either field is `nil` when that part could not be read: a package with no
repository link still has dependencies worth knowing, and an ecosystem whose
dependency shape is unhandled still has a repository to analyse. Returning a
pair rather than failing on the first missing half keeps them independent.

# `ecosystems`

```elixir
@spec ecosystems() :: [String.t()]
```

The ecosystems `resolve/3` understands.

Ask rather than assume: a caller that keeps its own list drifts the moment one
is added here, and refuses an ecosystem the library supports.

# `resolve`

```elixir
@spec resolve(String.t(), String.t(), keyword()) ::
  {:ok, String.t()}
  | {:error,
     :not_found
     | :no_repository
     | {:unsupported_ecosystem, String.t()}
     | {:unreachable, term()}
     | {:status, integer()}}
```

Resolve `package` in `ecosystem` to an https repository URL.

Options:
  * `:get` - the HTTP GET, for tests. Defaults to `HTTPoison.get/1`
    through `Lei.HTTP.Retry`.
  * `:retry` - retry options. Defaults to 3 attempts 2s apart: a registry
    that is down should fail a job quickly, not hold a worker for the
    retry module's default 5 attempts 15s apart.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
