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 oneIn-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.
Summary
Functions
Directness and in-degree for every package in a manifest.
Types
@type name() :: String.t()
A package identified as the manifest identifies it.
Functions
@spec classify([name()], %{optional(name()) => [name()] | nil}) :: %{ optional(name()) => %{ 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.