# `Agenda.Planner`
[🔗](https://github.com/elixir-tempo/agenda/blob/v0.3.0/lib/agenda/planner.ex#L1)

Working out when and where a session could be held.

The pipeline is ordered cheapest-first, and every stage but one is
set algebra Tempo already performs:

1. **Eligibility** — attribute matching, no calendar involved. This
   removes most of the search space before any interval is touched,
   and it is where a resource's own `requires` are folded in.

2. **Availability** — one `Tempo.difference/2` per surviving
   resource: what it is open for, minus what already claims it.

3. **Co-availability** — `Tempo.intersection/2` across a candidate
   combination: when the room *and* everyone needed are all free.

4. **Slotting** — cut those windows into placements of the session's
   length with `Tempo.IntervalSet.slots/3`.

5. **Ranking** — score against the session's soft preferences and
   return best-first.

Provenance rides on the intervals themselves. Each resource's free
time is tagged with its own name, and `Tempo.intersection/3`'s
`{:merge, fun}` resolver accumulates those tags as the sets are
intersected — so a surviving window states which resources produced
it, and `Tempo.IntervalSet.slots/3` carries that through to every
placement. Nothing has to be reconstructed afterwards.

`plan/3` ranks the ways a session could be held against a pool of
resources, and `conflict/3` gives the smallest set of demands that
rules out every one of them.

# `conflict`

```elixir
@spec conflict(Agenda.Session.t(), [Agenda.Resource.t()], keyword()) ::
  {:ok, [{:needs | :requires, {atom() | String.t(), atom()}}]} | :none
```

The smallest set of demands that rules out every way of holding
`session`.

Where `plan/3`'s failure says *that* nothing fits, this says *which
demands together* make it so — the requirement-level counterpart of
`Agenda.Arranger.conflict/3`. Two attributes that are each
satisfiable alone but impossible together are the common case, and
neither `Agenda.explain/2` nor a list of near misses will show it.

Both kinds of demand are searched, which matters because the second
is invisible at the call site:

* `{:needs, name, attribute}` — an attribute the session asked for,
  as in `Agenda.Session.needs(session, :room, seats: at_least(8))`.

* `{:requires, resource, attribute}` — an attribute a rostered
  resource *induces*, as in a person whose `requires:` tightens
  whatever room they are booked into.

### Arguments

* `session` is a `t:Agenda.Session.t/0`.

* `pool` is the list of `t:Agenda.Resource.t/0` to choose from.

* `options` is a keyword list of options.

### Options

* `:busy`, `:limit` and `:spread` are as for `plan/3`.

### Returns

* `:none` when the session can be held and there is nothing to
  explain, or

* `{:ok, demands}` — a minimal set of demands that cannot all be met.
  An empty list means no demand is to blame: the session fails on
  time alone, with every attribute demand dropped.

### Examples

    iex> import Agenda.Predicate
    iex> small = Agenda.resource("Snug", seats: 4, video_conferencing: true)
    iex> {:ok, small} = Agenda.open(small, "2026-06-15T09:00:00/2026-06-15T12:00:00")
    iex> big = Agenda.resource("Barn", seats: 40, video_conferencing: false)
    iex> {:ok, big} = Agenda.open(big, "2026-06-15T09:00:00/2026-06-15T12:00:00")
    iex> session =
    ...>   Agenda.session("Review", duration: "PT1H", window: "2026-06-15/2026-06-15")
    ...>   |> Agenda.Session.needs(:room, seats: at_least(8), video_conferencing: true)
    iex> Agenda.Planner.conflict(session, [small, big])
    {:ok, [needs: {:room, :seats}, needs: {:room, :video_conferencing}]}

# `plan`

```elixir
@spec plan(Agenda.Session.t(), [Agenda.Resource.t()], keyword()) ::
  {:ok, [Agenda.Arrangement.t()]} | {:error, Agenda.Infeasible.t()}
```

Rank the ways `session` could be held against `pool`.

### Arguments

* `session` is a `t:Agenda.Session.t/0` with a duration and a
  window.

* `pool` is the list of `t:Agenda.Resource.t/0` available to
  choose from.

* `options` is a keyword list of options.

### Options

* `:busy` is a map of resource name to what already claims it — any
  value `Agenda.Availability.free/2` accepts. The default is
  `%{}`.

* `:limit` is the most arrangements to return. The default is `20`.
  Truncation is reported rather than silent — see `:truncated?` on
  the result.

* `:spread` chooses *how* the list is truncated. `false`, the
  default, keeps the best `:limit` placements, which is what a caller
  picking one time wants. `true` samples start moments evenly across
  the window instead and round-robins between them, trading depth at
  the front of the window for coverage of all of it — which is what
  `Agenda.Arranger.arrange/3` needs, since sessions handed identical
  placements collide. The coverage is what makes a cap safe to set:
  with `:spread`, `limit: 4` over two days offers both days rather
  than four times on the first.

### Returns

* `{:ok, arrangements}` ranked best-first, or

* `{:error, t:Agenda.Infeasible.t/0}` carrying the reasons — which
  includes a session that never said when it may be held.

### Examples

    iex> session = Agenda.session("Review", duration: "PT1H")
    iex> {:error, reason} = Agenda.Planner.plan(session, [])
    iex> Agenda.explain(reason)
    "Review cannot be held: no window — say when it may be held, with window: on the session or by arranging it inside a programme"

    iex> boardroom = Agenda.Resource.new("Boardroom", seats: 8)
    iex> {:ok, boardroom} = Agenda.open(boardroom, "2026-06-15T09:00:00/2026-06-15T12:00:00")
    iex> session =
    ...>   Agenda.session("Review", duration: "PT1H", window: "2026-06-15/2026-06-15")
    ...>   |> Agenda.Session.needs(:room, seats: 8)
    iex> {:ok, arrangements} = Agenda.Planner.plan(session, [boardroom])
    iex> length(arrangements)
    3

---

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