Releaser.Graph (releaser v0.0.7)

Copy Markdown View Source

Dependency graph building and topological sorting for monorepo apps.

Builds a directed graph from internal path: dependencies and provides topological ordering for correct publish order, dependency resolution, and cascade planning.

New annotation helpers (for rendering):

Summary

Functions

Builds a dependency graph from a list of apps.

Shallow count of name's direct deps that themselves have at least one project-internal dep.

Returns the count of direct project-internal deps for a given app name.

Returns a map of %{app_name => [dependent_names]} (reverse graph).

Filters topological levels to only include the specified apps. Removes empty levels.

Inverts the topological_levels/1 result into a name-keyed map for O(1) level lookups.

Computes topological levels using Kahn's algorithm.

Resolves transitive dependents for a list of app names (reverse direction).

Resolves transitive dependencies for a list of app names.

Types

graph()

@type graph() :: %{required(name()) => [name()]}

levels()

@type levels() :: [{non_neg_integer(), [name()]}]

name()

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

Functions

build(apps)

Builds a dependency graph from a list of apps.

Returns a map of %{app_name => [dependency_names]}.

deep_count(name, graph)

@spec deep_count(name(), graph()) :: non_neg_integer()

Shallow count of name's direct deps that themselves have at least one project-internal dep.

SHALLOW count, NOT a recursive depth metric. For a chain a→b→c where c has no deps:

  • deep_count("a", graph) is 1 (b has deps)
  • deep_count("b", graph) is 0 (c has no deps)

See ADR D4 in the design document.

Returns 0 for leaves and unknown names.

Examples

iex> graph = %{"a" => ["b"], "b" => ["c"], "c" => []}
iex> Graph.deep_count("a", graph)
1
iex> Graph.deep_count("b", graph)
0

dep_count(name, graph)

@spec dep_count(name(), graph()) :: non_neg_integer()

Returns the count of direct project-internal deps for a given app name.

Returns 0 for unknown names (not present in the graph).

Examples

iex> Graph.dep_count("a", %{"a" => ["b", "c"], "b" => ["c"], "c" => []})
2

iex> Graph.dep_count("z", %{"a" => ["b"]})
0

dependents_of(apps)

Returns a map of %{app_name => [dependent_names]} (reverse graph).

For each app, lists which apps depend on it.

filter_levels(levels, required_apps)

Filters topological levels to only include the specified apps. Removes empty levels.

level_map(levels)

@spec level_map(levels()) :: %{required(name()) => non_neg_integer()}

Inverts the topological_levels/1 result into a name-keyed map for O(1) level lookups.

Examples

iex> Graph.level_map([{0, ["c", "d"]}, {1, ["b"]}, {2, ["a"]}])
%{"a" => 2, "b" => 1, "c" => 0, "d" => 0}

iex> Graph.level_map([])
%{}

topological_levels(apps)

Computes topological levels using Kahn's algorithm.

Returns [{level, [app_names]}] where level 0 has no internal deps, level 1 depends only on level 0, etc.

transitive_dependents(app_names, apps)

Resolves transitive dependents for a list of app names (reverse direction).

Given an app, returns all apps that depend on it recursively (upstream). For example, if cfdi_xml depends on cfdi_csd, then cfdi_xml is a transitive dependent of cfdi_csd.

This is the inverse of transitive_deps/2.

transitive_deps(app_names, graph)

Resolves transitive dependencies for a list of app names.

Returns a MapSet of all apps that need to be included (the requested apps plus all their transitive dependencies).