# `Choreo.Analysis`
[🔗](https://github.com/code-shoily/choreo/blob/v0.14.1/lib/choreo/analysis.ex#L1)

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

  * `single_points_of_failure/1` — articulation points and bridge edges
  * `impact_analysis/2` — what breaks if a node goes down
  * `centrality/2` — most coupled / critical services
  * `isolated_nodes/1` — orphan services with no connections
  * `validate/1` — structural health check

# `centrality`

```elixir
@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`

```elixir
@spec core_numbers(struct()) :: %{required(Yog.node_id()) =&gt; 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`

```elixir
@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?`

```elixir
@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?`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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?"

---

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