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

Which dependencies a customer chose, and which arrived with them (#263).

`risk_rank` orders a manifest by how sick each dependency is. It cannot say
whether the customer can do anything about it, and those are different
questions: a direct dependency was chosen and can be replaced, while a
transitive one may not be controllable without upstream moving.

## Why not in-degree

This issue originally proposed ranking by in-degree as a blast-radius proxy.
Measured against this repository's own 62 dependencies, that is
**anti-correlated** with what it was meant to find:

    highest in-degree:  jason 6 · telemetry 6 · decimal 5

    the seven filed as issues (#250-#254):
      git_cli, elixir_uuid, yarn_parser, poison, temp, mix_audit, json_xema
      -> in-degree 0, every one

In-degree measures how much other *dependencies* rely on a package, not how
much the *customer's code* does. `git_cli` is on every analysis path in the
product and nothing in the tree depends on it, because we do -- a
relationship no dependency graph contains.

What the measurement did show is that in-degree zero means direct, which is
the signal that made `git_cli` matter. Health says what is wrong;
directness says whether they can act on it.

In-degree is still reported, because it is real information about blast
radius within the tree. Neither is folded into the rank: both are facts
beside it, and the consumer's question decides which matters.

## What inference cannot see

Directness here is deduced, not declared. A lockfile knows which
dependencies are direct; the request does not carry it, so this reads it off
the graph instead.

That is sound for a complete manifest and wrong in a way nothing detects for
a partial one. Send only a transitive subset, or a tree with its roots
omitted, and every package has in-degree zero -- so every package is reported
as a deliberate choice, confidently, with no signal that the answer came from
a tree that was never whole.

Unlike unreadable edges, which announce themselves as `nil`, this failure
looks exactly like a correct answer. Accepting a declared `direct` flag
alongside the inference, and recording which was used, is the fix if a
partial manifest ever turns up. It is not built, because nothing has sent one
and the endpoint is documented for the whole tree.

# `name`

```elixir
@type name() :: String.t()
```

A package identified as the manifest identifies it.

# `classify`

```elixir
@spec classify([name()], %{optional(name()) =&gt; [name()] | nil}) :: %{
  optional(name()) =&gt; %{
    direct: boolean() | nil,
    in_degree: non_neg_integer() | nil
  }
}
```

Directness and in-degree for every package in a manifest.

`edges` maps a package to the packages it declares a dependency on. Only
edges pointing at packages **in this manifest** count: a dependency on
something the customer does not have says nothing about their tree.

Returns a map of package to `%{direct: boolean | nil, in_degree: non_neg_integer | nil}`.

A package with no edges known -- an unsupported ecosystem, a registry that
did not answer -- gets `nil` for both. **Unknown is not transitive.** Marking
it `direct: false` would quietly demote exactly the packages we know least
about, and a consumer filtering for "critical and direct" would never see
them.

---

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