# `Pipette.Graph`
[🔗](https://github.com/tommeier/pipette-buildkite-plugin/blob/main/lib/pipette/graph.ex#L1)

Directed Acyclic Graph (DAG) for pipeline dependency management.

Builds a dependency graph from groups and steps, supports cycle
detection, and computes transitive dependencies (ancestors).

## Example

    groups = [
      %Pipette.Group{name: :lint, steps: []},
      %Pipette.Group{name: :test, depends_on: :lint, steps: []},
      %Pipette.Group{name: :deploy, depends_on: :test, steps: []}
    ]

    graph = Pipette.Graph.from_groups(groups)
    Pipette.Graph.acyclic?(graph)          #=> true
    Pipette.Graph.ancestors(graph, :deploy) #=> MapSet.new([:test, :lint])

# `node_id`

```elixir
@type node_id() :: atom() | {atom(), atom()}
```

# `t`

```elixir
@type t() :: %Pipette.Graph{
  edges: %{required(node_id()) =&gt; MapSet.t(node_id())},
  nodes: MapSet.t(node_id())
}
```

# `acyclic?`

```elixir
@spec acyclic?(t()) :: boolean()
```

Return `true` if the graph has no cycles.

# `add_edge`

```elixir
@spec add_edge(t(), node_id(), node_id()) :: t()
```

Add a directed edge from `from` to `to`. Both nodes are added if not present.

# `add_node`

```elixir
@spec add_node(t(), node_id()) :: t()
```

Add a node to the graph. Returns the graph unchanged if the node already exists.

# `ancestors`

```elixir
@spec ancestors(t(), node_id()) :: MapSet.t(node_id())
```

Compute the transitive dependencies (ancestors) of a node.

Follows edges recursively to find all nodes that `node` depends on,
directly or transitively.

## Examples

    graph = Pipette.Graph.from_groups([
      %Pipette.Group{name: :lint, steps: []},
      %Pipette.Group{name: :test, depends_on: :lint, steps: []},
      %Pipette.Group{name: :deploy, depends_on: :test, steps: []}
    ])

    Pipette.Graph.ancestors(graph, :deploy)
    #=> MapSet.new([:test, :lint])

# `edges`

```elixir
@spec edges(t()) :: [{node_id(), node_id()}]
```

Return all edges as a list of `{from, to}` tuples.

# `find_cycle`

```elixir
@spec find_cycle(t()) :: [node_id()] | nil
```

Find a cycle in the graph, if one exists.

Returns a list of nodes forming the cycle path, or `nil` if the graph
is acyclic. Uses DFS with three-color marking.

# `from_groups`

```elixir
@spec from_groups([Pipette.Group.t()]) :: t()
```

Build a dependency graph from a list of groups.

Creates nodes for each group and step, with edges representing
`depends_on` relationships at both the group and step level.

## Examples

    groups = [
      %Pipette.Group{name: :lint, steps: []},
      %Pipette.Group{name: :test, depends_on: :lint, steps: []}
    ]

    graph = Pipette.Graph.from_groups(groups)
    Pipette.Graph.has_edge?(graph, :test, :lint)  #=> true

# `has_edge?`

```elixir
@spec has_edge?(t(), node_id(), node_id()) :: boolean()
```

Check if a directed edge exists from `from` to `to`.

# `has_node?`

```elixir
@spec has_node?(t(), node_id()) :: boolean()
```

Check if a node exists in the graph.

# `new`

```elixir
@spec new() :: t()
```

Create an empty graph.

---

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