Analysis functions for Choreo.Dependency graphs.
Provides algorithms that answer practical questions about a codebase:
- Where are the circular dependencies?
- What breaks if I change a component?
- Are we violating our layer architecture?
- Which components are most coupled?
- What is the deepest dependency chain?
Further reading
Summary
Functions
Returns all nodes that transitively depend on the given node.
Returns nodes ranked by coupling centrality (in-degree + out-degree).
Returns all circular dependency chains.
Returns all nodes that the given node transitively depends on.
Calculates the Instability metric for each component.
Identifies completely isolated subsystems (weakly connected components).
Checks edges against an expected layered architecture.
Returns nodes with no dependents (nothing depends on them).
Finds the longest dependency chain in the graph.
Returns nodes with no dependencies (foundational components).
Identifies redundant explicit dependencies.
Validates a dependency graph and returns a list of issues.
Functions
@spec affected_by(Choreo.Dependency.t(), Yog.node_id()) :: [Yog.node_id()]
Returns all nodes that transitively depend on the given node.
If you change target, these are the components that could break.
Examples
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_application(:api)
...> |> Choreo.Dependency.add_module(:auth)
...> |> Choreo.Dependency.add_module(:util)
...> |> Choreo.Dependency.depends_on(:api, :auth)
...> |> Choreo.Dependency.depends_on(:auth, :util)
iex> Enum.sort(Choreo.Dependency.Analysis.affected_by(deps, :util))
[:api, :auth]
iex> Choreo.Dependency.Analysis.affected_by(deps, :api)
[]This analysis answers the question: "What breaks if I change this component?"
@spec centrality( Choreo.Dependency.t(), keyword() ) :: [Yog.node_id()]
Returns nodes ranked by coupling centrality (in-degree + out-degree).
The most coupled components are at the head of the list.
Options
:limit— maximum number of results (default: all)
Examples
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_module(:hub)
...> |> Choreo.Dependency.add_module(:a)
...> |> Choreo.Dependency.add_module(:b)
...> |> Choreo.Dependency.add_module(:c)
...> |> Choreo.Dependency.depends_on(:a, :hub)
...> |> Choreo.Dependency.depends_on(:b, :hub)
...> |> Choreo.Dependency.depends_on(:hub, :c)
iex> hd(Choreo.Dependency.Analysis.centrality(deps))
:hub
iex> length(Choreo.Dependency.Analysis.centrality(deps, limit: 2))
2This analysis answers the question: "Which components are the most coupled?"
@spec cyclic_dependencies(Choreo.Dependency.t()) :: [[Yog.node_id()]]
Returns all circular dependency chains.
Each cycle is returned as a list of node IDs starting and ending at the same node. Only one representative cycle is returned per strongly connected component.
Examples
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_module(:a)
...> |> Choreo.Dependency.add_module(:b)
...> |> Choreo.Dependency.add_module(:c)
...> |> Choreo.Dependency.depends_on(:a, :b)
...> |> Choreo.Dependency.depends_on(:b, :a)
...> |> Choreo.Dependency.depends_on(:c, :a)
iex> cycles = Choreo.Dependency.Analysis.cyclic_dependencies(deps)
iex> length(cycles)
1
iex> [cycle] = cycles
iex> hd(cycle) == List.last(cycle)
true
iex> :a in cycle
true
iex> :b in cycle
trueThis analysis answers the question: "Where are the circular dependencies?"
@spec depends_on(Choreo.Dependency.t(), Yog.node_id()) :: [Yog.node_id()]
Returns all nodes that the given node transitively depends on.
These are the components target cannot function without.
Examples
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_application(:api)
...> |> Choreo.Dependency.add_module(:auth)
...> |> Choreo.Dependency.add_module(:util)
...> |> Choreo.Dependency.depends_on(:api, :auth)
...> |> Choreo.Dependency.depends_on(:auth, :util)
iex> Enum.sort(Choreo.Dependency.Analysis.depends_on(deps, :api))
[:auth, :util]
iex> Choreo.Dependency.Analysis.depends_on(deps, :util)
[]This analysis answers the question: "What does this component need to function?"
@spec instability(Choreo.Dependency.t()) :: %{required(Yog.node_id()) => float()}
Calculates the Instability metric for each component.
Formula: Efferent Coupling / (Afferent + Efferent Coupling)
Returns a map of node_id => instability_score.
A score of 0.0 means the component is maximally stable.
A score of 1.0 means the component is maximally unstable.
Examples
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_module(:stable)
...> |> Choreo.Dependency.add_module(:unstable)
...> |> Choreo.Dependency.add_module(:mixed)
...> |> Choreo.Dependency.depends_on(:unstable, :stable)
...> |> Choreo.Dependency.depends_on(:unstable, :mixed)
...> |> Choreo.Dependency.depends_on(:mixed, :stable)
iex> scores = Choreo.Dependency.Analysis.instability(deps)
iex> scores.stable
0.0
iex> scores.unstable
1.0
iex> scores.mixed
0.5This analysis answers the question: "How stable is each component?"
@spec isolated_subsystems(Choreo.Dependency.t()) :: [[Yog.node_id()]]
Identifies completely isolated subsystems (weakly connected components).
Returns a list of components, where each component is a list of node IDs.
Examples
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_module(:a1)
...> |> Choreo.Dependency.add_module(:a2)
...> |> Choreo.Dependency.depends_on(:a1, :a2)
...> |> Choreo.Dependency.add_module(:b1)
...> |> Choreo.Dependency.add_module(:b2)
...> |> Choreo.Dependency.depends_on(:b1, :b2)
...> |> Choreo.Dependency.add_module(:orphan)
iex> subsystems = Choreo.Dependency.Analysis.isolated_subsystems(deps)
iex> length(subsystems)
3
iex> Enum.any?(subsystems, fn g -> Enum.sort(g) == [:a1, :a2] end)
true
iex> Enum.any?(subsystems, fn g -> g == [:orphan] end)
trueThis analysis answers the question: "Which groups of components are completely disconnected?"
@spec layer_violations(Choreo.Dependency.t(), %{required(Yog.node_id()) => integer()}) :: [ {Yog.node_id(), Yog.node_id(), String.t()} ]
Checks edges against an expected layered architecture.
layers is a map of node_id => layer_index where lower numbers are
"lower" layers (foundations). A violation occurs when an edge points
from a lower layer to a higher layer (the dependency arrow goes
upward).
Returns a list of violation tuples: {from, to, description}.
Examples
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_module(:repo)
...> |> Choreo.Dependency.add_module(:service)
...> |> Choreo.Dependency.add_module(:api)
...> |> Choreo.Dependency.depends_on(:service, :repo)
...> |> Choreo.Dependency.depends_on(:repo, :api)
iex> layers = %{repo: 1, service: 2, api: 3}
iex> violations = Choreo.Dependency.Analysis.layer_violations(deps, layers)
iex> length(violations)
1
iex> {from, to, _desc} = hd(violations)
iex> from
:repo
iex> to
:apiThis analysis answers the question: "Which edges violate my layered architecture?"
@spec leaves(Choreo.Dependency.t()) :: [Yog.node_id()]
Returns nodes with no dependents (nothing depends on them).
These are safe to change or delete without breaking other components.
Examples
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_module(:a)
...> |> Choreo.Dependency.add_module(:b)
...> |> Choreo.Dependency.add_module(:c)
...> |> Choreo.Dependency.depends_on(:a, :b)
...> |> Choreo.Dependency.depends_on(:b, :c)
iex> Choreo.Dependency.Analysis.leaves(deps)
[:a]This analysis answers the question: "Which components have no dependents?"
@spec longest_dependency_chain(Choreo.Dependency.t()) :: {:ok, [Yog.node_id()], number()} | :error
Finds the longest dependency chain in the graph.
This measures the maximum depth of the dependency tree — useful for estimating compilation or test ordering time.
Returns {:ok, [id], total_weight} or :error if cyclic.
Examples
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_module(:a)
...> |> Choreo.Dependency.add_module(:b)
...> |> Choreo.Dependency.add_module(:c)
...> |> Choreo.Dependency.depends_on(:a, :b)
...> |> Choreo.Dependency.depends_on(:b, :c)
iex> Choreo.Dependency.Analysis.longest_dependency_chain(deps)
{:ok, [:a, :b, :c], 2}
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_module(:a)
...> |> Choreo.Dependency.add_module(:b)
...> |> Choreo.Dependency.depends_on(:a, :b)
...> |> Choreo.Dependency.depends_on(:b, :a)
iex> Choreo.Dependency.Analysis.longest_dependency_chain(deps)
:errorThis analysis answers the question: "What is the deepest dependency chain?"
@spec roots(Choreo.Dependency.t()) :: [Yog.node_id()]
Returns nodes with no dependencies (foundational components).
These are the bottom of the dependency stack.
Examples
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_module(:a)
...> |> Choreo.Dependency.add_module(:b)
...> |> Choreo.Dependency.add_module(:c)
...> |> Choreo.Dependency.depends_on(:a, :b)
...> |> Choreo.Dependency.depends_on(:b, :c)
iex> Choreo.Dependency.Analysis.roots(deps)
[:c]This analysis answers the question: "Which components have no dependencies?"
@spec transitive_reduction(Choreo.Dependency.t()) :: [{Yog.node_id(), Yog.node_id()}]
Identifies redundant explicit dependencies.
If A depends on B, B depends on C, and A depends on C, the direct
edge A -> C is often redundant. This returns a list of {from, to}
tuples representing these redundant edges.
Examples
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_module(:a)
...> |> Choreo.Dependency.add_module(:b)
...> |> Choreo.Dependency.add_module(:c)
...> |> Choreo.Dependency.depends_on(:a, :b)
...> |> Choreo.Dependency.depends_on(:b, :c)
...> |> Choreo.Dependency.depends_on(:a, :c)
iex> Choreo.Dependency.Analysis.transitive_reduction(deps)
[{:a, :c}]This analysis answers the question: "Which explicit dependencies are redundant?"
@spec validate(Choreo.Dependency.t()) :: [{:error | :warning, String.t()}]
Validates a dependency graph and returns a list of issues.
Checks for:
- circular dependencies
- orphaned nodes (no edges at all)
Returns a list of {severity, message} tuples.
Examples
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_module(:a)
...> |> Choreo.Dependency.add_module(:b)
...> |> Choreo.Dependency.depends_on(:a, :b)
iex> Choreo.Dependency.Analysis.validate(deps)
[]
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...> |> Choreo.Dependency.add_module(:a)
...> |> Choreo.Dependency.add_module(:b)
...> |> Choreo.Dependency.depends_on(:a, :b)
...> |> Choreo.Dependency.depends_on(:b, :a)
iex> issues = Choreo.Dependency.Analysis.validate(deps)
iex> Enum.any?(issues, fn {sev, msg} -> sev == :error and String.contains?(msg, "Circular") end)
trueThis analysis answers the question: "Is the dependency graph structurally sound?"