Choreo.Analysis (Choreo v0.8.0)

Copy Markdown View Source

Graph-algorithm wrappers for Choreo architecture diagrams.

These functions adapt the generic algorithms from Yog to the domain of system architecture.

Semiring support

Functions that deal with path weights (e.g. shortest_path/4, mst/2) accept semiring options so you can define "shortest" in terms of cost, latency, or any custom metric:

# Shortest by cost (default — numeric addition)
Choreo.Analysis.shortest_path(system, :api, :db)

# Shortest by latency stored in edge meta
Choreo.Analysis.shortest_path(system, :api, :db,
  zero: 0,
  add: &Kernel.+/2,
  compare: &Yog.Utils.compare/2
)

Domain-specific analysis

Summary

Functions

Calculates centrality scores for nodes in the diagram.

Calculates the core number for each node in the diagram.

Finds articulation points (cut vertices) in the diagram.

Detects cycles in the system.

Returns true if the system is a Directed Acyclic Graph (DAG). This analysis answers the question: "Can I safely order my services linearly?"

Generates a "heat-mapped" version of a diagram by coloring nodes based on their centrality scores.

Converts a path result into DOT highlighting options.

Returns all nodes that are transitively affected if target goes down.

Returns nodes with no connections (zero in-degree and out-degree).

Returns a standalone Choreo diagram acting as a color legend for a heatmap palette.

Computes a Minimum Spanning Tree (MST) of the system.

Finds a path between nodes based on domain-specific criteria.

Performs transitive reduction on the diagram's dependencies.

Finds the shortest (cheapest) path between two services.

Identifies single points of failure in the architecture.

Returns the strongly-connected components of the system.

Returns a topological ordering of the system nodes.

Validates the system architecture and returns a list of issues.

Functions

centrality(diagram, opts \\ [])

@spec centrality(struct(), keyword()) :: [{Yog.node_id(), float()}]

Calculates centrality scores for nodes in the diagram.

Centrality identifies the most important or "central" nodes in a graph. Supported measures include:

  • :measure — one of:
    • :degree (default) — simple connectivity count
    • :betweenness — bridge/gatekeeper detection
    • :closeness — distance-based importance
    • :pagerank — link-quality importance (directed graphs)
    • :spof — single point of failure (cut vertex) detection
    • :k_core — nucleus/cluster density analysis
  • :limit — return only the top N results
  • :mode — for degree centrality: :in_degree, :out_degree, or :total_degree (default)
  • Semiring options (:zero, :add, :compare, :to_float) for betweenness and closeness

Returns a list of {node_id, score} tuples sorted by score descending.

Examples

iex> system = Choreo.new() |> Choreo.add_service(:a) |> Choreo.add_service(:b) |> Choreo.add_service(:c)
iex> system = system |> Choreo.connect(:a, :b) |> Choreo.connect(:c, :b)
iex> [{top, _}] = Choreo.Analysis.centrality(system, limit: 1)
iex> top
:b

This analysis answers the question: "Which nodes are the most critical connectors?"

core_numbers(diagram)

@spec core_numbers(struct()) :: %{required(Yog.node_id()) => integer()}

Calculates the core number for each node in the diagram.

The core number of a node is the largest value k such that the node belongs to a maximal subgraph where every node has degree at least k.

Returns a map of node_id => core_number.

cut_vertices(diagram)

@spec cut_vertices(struct()) :: [Yog.node_id()]

Finds articulation points (cut vertices) in the diagram.

An articulation point is a node whose removal increases the number of connected components in the graph. In architectural terms, these are Single Points of Failure (SPOFs).

Returns a list of node IDs.

Examples

iex> system = Choreo.new()
iex> system = system |> Choreo.add_service(:a) |> Choreo.add_service(:b) |> Choreo.add_service(:c)
iex> system = system |> Choreo.connect(:a, :b) |> Choreo.connect(:b, :c)
iex> Choreo.Analysis.cut_vertices(system)
[:b]

cyclic?(system)

@spec cyclic?(Choreo.t()) :: boolean()

Detects cycles in the system.

Returns true if the system contains at least one cycle. This analysis answers the question: "Does my architecture contain a feedback loop?"

dag?(system)

@spec dag?(Choreo.t()) :: boolean()

Returns true if the system is a Directed Acyclic Graph (DAG). This analysis answers the question: "Can I safely order my services linearly?"

heatmap(diagram, opts \\ [])

@spec heatmap(struct(), keyword()) :: struct()

Generates a "heat-mapped" version of a diagram by coloring nodes based on their centrality scores.

Options

  • :measure — Centrality measure to use (:degree, :betweenness, etc.)
  • :palette — Color palette (:heat, :cool, :spectral)
  • All other options are passed to centrality/2.

Examples

# Highlight high-degree services in a system diagram
system = Choreo.new() |> ...
heat_system = Choreo.Analysis.heatmap(system, palette: :heat)

This analysis answers the question: "Where are the hotspots in my architecture?"

highlight(arg1)

@spec highlight(map()) :: keyword()

Converts a path result into DOT highlighting options.

Returns a list of options suitable for passing to to_dot/2.

impact_analysis(system, target)

@spec impact_analysis(Choreo.t(), Yog.node_id()) :: [Yog.node_id()]

Returns all nodes that are transitively affected if target goes down.

Uses BFS on the transposed graph: if A → B → C and target is B, then A depends (transitively) on B, so A is affected.

## Examples

  iex> system =
  ...>   Choreo.new()
  ...>   |> Choreo.add_service(:api)
  ...>   |> Choreo.add_service(:auth)
  ...>   |> Choreo.add_database(:db)
  ...>   |> Choreo.connect(:api, :auth)
  ...>   |> Choreo.connect(:auth, :db)
  iex> Choreo.Analysis.impact_analysis(system, :db) |> Enum.sort()
  [:api, :auth]

This analysis answers the question: "What breaks if this service goes down?"

isolated_nodes(system)

@spec isolated_nodes(Choreo.t()) :: [Yog.node_id()]

Returns nodes with no connections (zero in-degree and out-degree).

Isolated services in an architecture diagram are usually a mistake — either a missing connection or an orphaned component.

## Examples

  iex> system =
  ...>   Choreo.new()
  ...>   |> Choreo.add_service(:connected)
  ...>   |> Choreo.add_service(:orphan)
  ...>   |> Choreo.connect(:connected, :connected)
  iex> Choreo.Analysis.isolated_nodes(system)
  [:orphan]

This analysis answers the question: "Which services have no connections at all?"

legend(opts_or_palette \\ :heat)

Returns a standalone Choreo diagram acting as a color legend for a heatmap palette.

Examples

iex> legend = Choreo.Analysis.legend(:heat)
iex> Choreo.to_dot(legend) =~ "Legend"

mst(system, opts \\ [])

@spec mst(
  Choreo.t(),
  keyword()
) :: {:ok, Yog.MST.Result.t()} | {:error, atom() | String.t()}

Computes a Minimum Spanning Tree (MST) of the system.

The MST is calculated on an undirected copy of the graph using Kruskal's algorithm. Edge costs are the weights given to connect/4.

Returns {:ok, Yog.MST.Result.t()} or {:error, reason}.

## Options

* `:algorithm` - `:kruskal` (default), `:prim`, `:boruvka`
* `:compare` - custom comparison function for edge weights

## Examples

  iex> system =
  ...>   Choreo.new(directed: false)
  ...>   |> Choreo.add_database(:db)
  ...>   |> Choreo.add_service(:api)
  ...>   |> Choreo.add_cache(:cache)
  ...>   |> Choreo.connect(:api, :db, cost: 10)
  ...>   |> Choreo.connect(:api, :cache, cost: 5)
  ...>   |> Choreo.connect(:db, :cache, cost: 20)
  iex> {:ok, mst} = Choreo.Analysis.mst(system)
  iex> mst.total_weight
  15

This analysis answers the question: "What is the cheapest way to connect all services?"

path(diagram, from, to, opts \\ [])

@spec path(struct(), Yog.node_id(), Yog.node_id(), keyword()) :: {:ok, map()} | :error

Finds a path between nodes based on domain-specific criteria.

Supported measures:

  • :shortest (default): Minimal number of hops.
  • :latency: Minimal cumulative latency (uses latency_ms for Workflows/Dataflows).
  • :throughput: Maximal bottleneck capacity (uses rate for Dataflows).
  • :risk: Path through nodes with minimal security risk (ThreatModel).

Returns {:ok, path} where path is a Yog.Pathfinding.Path struct, or :error.

Examples

iex> system = Choreo.new() |> Choreo.add_service(:a) |> Choreo.add_service(:b) |> Choreo.connect(:a, :b)
iex> {:ok, path} = Choreo.Analysis.path(system, :a, :b)
iex> path.nodes
[:a, :b]

reduce_transitive(diagram)

@spec reduce_transitive(struct()) :: {:ok, struct()} | {:error, :contains_cycle}

Performs transitive reduction on the diagram's dependencies.

Transitive reduction removes redundant edges while preserving reachability. If A -> B, B -> C, and A -> C, then the edge A -> C is removed as it is implied by the other two.

Returns {:ok, diagram} with redundant edges removed, or {:error, :contains_cycle} if the diagram is not a DAG.

Examples

iex> system = Choreo.new()
iex> system = system |> Choreo.add_service(:a) |> Choreo.add_service(:b) |> Choreo.add_service(:c)
iex> system = system |> Choreo.connect(:a, :b) |> Choreo.connect(:b, :c) |> Choreo.connect(:a, :c)
iex> {:ok, reduced} = Choreo.Analysis.reduce_transitive(system)
iex> Enum.count(Choreo.edges(reduced))
2

shortest_path(system, from, to, opts \\ [])

@spec shortest_path(Choreo.t(), Yog.node_id(), Yog.node_id(), keyword()) ::
  {:ok, map()} | :error

Finds the shortest (cheapest) path between two services.

Supports semiring-parameterised weights: pass :zero, :add, and :compare to define "shortest" in terms of cost, latency, or any custom metric.

Returns {:ok, path} where path has .nodes and .weight, or :error if no path exists.

## Options

* `:zero`  identity element for the weight type (default: `0`)
* `:add`  function to combine two weights (default: `&Kernel.+/2`)
* `:compare`  function returning `:lt`, `:eq`, `:gt` (default: `&Yog.Utils.compare/2`)

## Examples

  iex> system =
  ...>   Choreo.new()
  ...>   |> Choreo.add_service(:api)
  ...>   |> Choreo.add_service(:auth)
  ...>   |> Choreo.add_database(:db)
  ...>   |> Choreo.connect(:api, :auth, cost: 5)
  ...>   |> Choreo.connect(:auth, :db, cost: 10)
  iex> {:ok, path} = Choreo.Analysis.shortest_path(system, :api, :db)
  iex> path.nodes
  [:api, :auth, :db]
  iex> path.weight
  15

This analysis answers the question: "What is the fastest or cheapest route between two services?"

single_points_of_failure(system)

@spec single_points_of_failure(Choreo.t()) :: %{
  nodes: [Yog.node_id()],
  edges: [{Yog.node_id(), Yog.node_id()}]
}

Identifies single points of failure in the architecture.

Returns a map with:

* `:nodes`  articulation points: services whose removal would
  partition the system into disconnected components
* `:edges`  bridge edges: connections whose removal would split
  the system

Uses Tarjan's algorithm on an undirected view of the graph.

## Examples

  iex> system =
  ...>   Choreo.new()
  ...>   |> Choreo.add_service(:api)
  ...>   |> Choreo.add_service(:auth)
  ...>   |> Choreo.add_database(:db)
  ...>   |> Choreo.connect(:api, :auth)
  ...>   |> Choreo.connect(:auth, :db)
  iex> result = Choreo.Analysis.single_points_of_failure(system)
  iex> result.nodes
  [:auth]
  iex> result.edges
  [{:api, :auth}, {:auth, :db}]

This analysis answers the question: "Which services or links would take down the whole system if they failed?"

strongly_connected_components(system)

@spec strongly_connected_components(Choreo.t()) :: [[Yog.node_id()]]

Returns the strongly-connected components of the system.

Each component is a list of node IDs that are mutually reachable. Single-node components indicate nodes that are not part of a cycle.

## Examples

  components = Choreo.Analysis.strongly_connected_components(system)

This analysis answers the question: "Which services are mutually dependent?"

topological_sort(system)

@spec topological_sort(Choreo.t()) :: {:ok, [Yog.node_id()]} | {:error, String.t()}

Returns a topological ordering of the system nodes.

This is useful for determining execution order in data-flow pipelines. The system graph must be a DAG; if cycles exist, {:error, reason} is returned.

## Examples

  iex> system =
  ...>   Choreo.new()
  ...>   |> Choreo.add_service(:ingest)
  ...>   |> Choreo.add_service(:transform)
  ...>   |> Choreo.add_service(:store)
  ...>   |> Choreo.add_dataflow(:ingest, :transform)
  ...>   |> Choreo.add_dataflow(:transform, :store)
  iex> {:ok, order} = Choreo.Analysis.topological_sort(system)
  iex> order
  [:ingest, :transform, :store]

This analysis answers the question: "In what order should I deploy or initialise services?"

validate(system)

@spec validate(Choreo.t()) :: [{:error | :warning, String.t()}]

Validates the system architecture and returns a list of issues.

Checks for:

* isolated nodes (no connections)
* single points of failure (articulation points)
* cycles in a directed system
* bridge edges (single connection between components)

Returns a list of {severity, message} tuples.

## Examples

  iex> system =
  ...>   Choreo.new()
  ...>   |> Choreo.add_service(:api)
  ...>   |> Choreo.add_service(:auth)
  ...>   |> Choreo.add_database(:db)
  ...>   |> Choreo.connect(:api, :auth)
  ...>   |> Choreo.connect(:auth, :db)
  iex> [{_severity, msg} | _rest] = Choreo.Analysis.validate(system)
  iex> String.contains?(msg, "Bridge edges")
  true

This analysis answers the question: "Is my architecture structurally sound?"