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

Graph analysis algorithms for `Choreo.Planner` projects.

Provides project-management-specific insights:

  * `ready/1` — tasks whose dependencies are all done
  * `blocked/1` — tasks with unresolved dependencies
  * `orphans/1` — tasks not in any milestone
  * `critical_path/2` — longest dependency chain (by estimate)
  * `bottlenecks/1` — tasks ranked by transitive downstream impact
  * `workload_by_assignee/2` — open work grouped by owner
  * `validate/1` — structural integrity checks

# `blocked`

```elixir
@spec blocked(Choreo.Planner.t()) :: [{Yog.node_id(), map()}]
```

Returns tasks that have unresolved dependencies or blockers.

Done and cancelled tasks are excluded.

## Examples

    iex> project = Choreo.Planner.new()
    ...> |> Choreo.Planner.add_task(:a, status: :backlog)
    ...> |> Choreo.Planner.add_task(:b, status: :backlog)
    ...> |> Choreo.Planner.depends_on(:a, :b)
    iex> [{id, _}] = Choreo.Planner.Analysis.blocked(project)
    iex> id
    :a

# `bottlenecks`

```elixir
@spec bottlenecks(Choreo.Planner.t()) :: [{Yog.node_id(), non_neg_integer()}]
```

Returns tasks sorted by how many other tasks depend on them
(directly or transitively).

High count = task blocks a lot of downstream work (bottleneck).

## Examples

    iex> project = Choreo.Planner.new()
    ...> |> Choreo.Planner.add_task(:core)
    ...> |> Choreo.Planner.add_task(:a)
    ...> |> Choreo.Planner.add_task(:b)
    ...> |> Choreo.Planner.depends_on(:a, :core)
    ...> |> Choreo.Planner.depends_on(:b, :core)
    iex> [{top_id, top_count} | _rest] = Choreo.Planner.Analysis.bottlenecks(project)
    iex> top_id
    :core
    iex> top_count
    2

# `critical_path`

```elixir
@spec critical_path(Choreo.Planner.t(), keyword()) ::
  {:ok, [Yog.node_id()], keyword()} | :error
```

Finds the critical path — the longest chain of dependent work by estimate.

If a `milestone` is given, only tasks under that milestone are considered.

Returns `{:ok, path, total_estimate}` or `:error` if the dependency graph
contains a cycle.

## Examples

    iex> project = Choreo.Planner.new()
    ...> |> Choreo.Planner.add_task(:a, estimate_hours: 2)
    ...> |> Choreo.Planner.add_task(:b, estimate_hours: 3)
    ...> |> Choreo.Planner.depends_on(:b, :a)
    iex> {:ok, [:a, :b], total_estimate: 5} = Choreo.Planner.Analysis.critical_path(project)

    iex> project = Choreo.Planner.new()
    ...> |> Choreo.Planner.add_task(:a)
    ...> |> Choreo.Planner.add_task(:b)
    ...> |> Choreo.Planner.add_task(:c)
    ...> |> Choreo.Planner.depends_on(:b, :a)
    ...> |> Choreo.Planner.depends_on(:a, :c)
    ...> |> Choreo.Planner.depends_on(:c, :b)
    iex> Choreo.Planner.Analysis.critical_path(project)
    :error

# `orphans`

```elixir
@spec orphans(Choreo.Planner.t()) :: [{Yog.node_id(), map()}]
```

Returns tasks with no `:contains` relationship (not in any milestone).

## Examples

    iex> project = Choreo.Planner.new() |> Choreo.Planner.add_task(:t1)
    iex> [{id, _}] = Choreo.Planner.Analysis.orphans(project)
    iex> id
    :t1

# `ready`

```elixir
@spec ready(Choreo.Planner.t()) :: [{Yog.node_id(), map()}]
```

Returns tasks with status `:backlog` or `:todo` whose dependencies
are all `:done`.

## Examples

    iex> project = Choreo.Planner.new()
    ...> |> Choreo.Planner.add_task(:t1, status: :todo)
    iex> [{id, _}] = Choreo.Planner.Analysis.ready(project)
    iex> id
    :t1

# `validate`

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

Evaluates the planner and returns a list of human-readable warnings and errors.

## Examples

    iex> project = Choreo.Planner.new() |> Choreo.Planner.add_task(:a, status: :in_progress)
    iex> Choreo.Planner.Analysis.validate(project)
    [
      {:warning, "Task :a is in progress but has no assignee"},
      {:warning, "Task :a is not in any milestone"}
    ]

    iex> project = Choreo.Planner.new()
    ...> |> Choreo.Planner.add_task(:a)
    ...> |> Choreo.Planner.add_task(:b)
    ...> |> Choreo.Planner.add_task(:c)
    ...> |> Choreo.Planner.depends_on(:b, :a)
    ...> |> Choreo.Planner.depends_on(:a, :c)
    ...> |> Choreo.Planner.depends_on(:c, :b)
    iex> [{:error, msg} | _] = Choreo.Planner.Analysis.validate(project)
    iex> String.contains?(msg, "Cycle detected")
    true

# `validate_messages`

```elixir
@spec validate_messages(Choreo.Planner.t()) :: [{:error | :warning, String.t()}]
```

Converts validation 3-tuples to standard 2-tuples with human-readable messages.

# `workload_by_assignee`

```elixir
@spec workload_by_assignee(Choreo.Planner.t(), keyword()) :: [
  {Yog.node_id() | :unassigned,
   %{
     task_count: non_neg_integer(),
     tasks: [Yog.node_id()],
     estimate_hours: number(),
     status_counts: %{optional(atom()) =&gt; non_neg_integer()}
   }}
]
```

Groups open work by assignee with task counts, estimates, and status counts.

By default this excludes `:done` and `:cancelled` tasks so the result reflects
remaining workload. Pass `include_done?: true` to include every task. Tasks with
multiple assignees are counted once for each assignee; unassigned tasks are
grouped under `:unassigned`.

## Examples

    iex> project = Choreo.Planner.new()
    ...> |> Choreo.Planner.add_task(:a, status: :todo, estimate_hours: 2)
    ...> |> Choreo.Planner.add_task(:b, status: :done, estimate_hours: 3)
    ...> |> Choreo.Planner.add_user(:alice)
    ...> |> Choreo.Planner.assign(:a, :alice)
    iex> Choreo.Planner.Analysis.workload_by_assignee(project)
    [alice: %{estimate_hours: 2, status_counts: %{todo: 1}, task_count: 1, tasks: [:a]}]

---

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