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

What is currently allocated, and to whom.

The ledger is an ordinary immutable value holding
`t:Agenda.Allocation.t/0` records **keyed by session**. Two
consequences follow from that key, and they are the whole point of
the module:

* **Releasing is dropping a key.** Cancelling a session frees every
  resource it held, because nothing else was holding them. There is
  no release step to forget and no free/busy record to go stale —
  free time is derived by `Agenda.Availability.free/2`, never
  stored.

* **Moving a session is a changeset, not a rewrite.** `diff/3`
  reports only what genuinely changed. The naive alternative —
  release everything, re-acquire it — loses the room to a competing
  booking in the gap and churns rows that did not move.

The ledger never writes anything. `diff/3` returns a plain value for
a persistence layer to apply inside its own transaction.

### The loop

    iex> boardroom = Agenda.resource("Boardroom", seats: 8)
    iex> {:ok, boardroom} = Agenda.open(boardroom, "2026-06-15T09:00:00/2026-06-15T11:00:00")
    iex> session =
    ...>   Agenda.session("Review", duration: "PT1H", window: "2026-06-15/2026-06-15")
    ...>   |> Agenda.Session.needs(:room, seats: 8)
    iex> ledger = Agenda.Ledger.new()
    iex> {:ok, [best | _]} = Agenda.plan(session, [boardroom], busy: Agenda.busy(ledger))
    iex> {:ok, ledger} = Agenda.Ledger.allocate(ledger, best)
    iex> Agenda.Ledger.count(ledger)
    1

> *"Plan against what is already allocated, then allocate the best
> option."*

### In iCalendar

A session's life in the ledger is its `STATUS`: `hold/3` makes it
`TENTATIVE`, `confirm/2` `CONFIRMED`, and `cancel/2` `CANCELLED`
(`release/2` is the same freeing, named from the resources' side).
The concepts match one for one. A hold's `held_until` has no
counterpart: iCalendar can say a session is tentative, not until
when, so an export drops the expiry and `expire/2` has nothing to
act on afterwards.

# `busy_type`

```elixir
@type busy_type() :: :busy | :tentative
```

Whether busy time is firm or only held: iCalendar's `BUSY` and
`BUSY-TENTATIVE` (RFC 5545 `FBTYPE`, RFC 7953 `BUSYTYPE`).

# `change`

```elixir
@type change() :: {:keep | :release | :allocate, Agenda.Allocation.t()}
```

One line of a changeset: keep a binding untouched, give one back, or
take a new one.

# `t`

```elixir
@type t() :: %Agenda.Ledger{
  sessions: %{optional(String.t()) =&gt; [Agenda.Allocation.t()]}
}
```

Allocations held against the sessions that hold them.

# `allocate`

```elixir
@spec allocate(t(), Agenda.Arrangement.t(), keyword()) :: {:ok, t()}
```

Record an arrangement, replacing whatever that session held before.

Allocating is idempotent: allocating the same arrangement twice
leaves the ledger identical, because a session's allocations are
replaced wholesale rather than appended.

### Arguments

* `ledger` is a `t:t/0`.

* `arrangement` is a `t:Agenda.Arrangement.t/0`.

* `options` is a keyword list of options.

### Options

* `:tag` is what the claim was for, as a `t:Agenda.Allocation.tag/0`
  — `{:project, "ACME-2026-01"}`, `{:leave, :annual}`. Recorded on
  every allocation the arrangement implies, and grouped on by
  `Agenda.reconcile/3`. The default is `nil`.

### Returns

* `{:ok, ledger}`.

### Examples

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.resource("Boardroom")
    iex> arrangement = %Agenda.Arrangement{
    ...>   session: "Review",
    ...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> {:ok, ledger} = Agenda.Ledger.allocate(Agenda.Ledger.new(), arrangement)
    iex> Agenda.Ledger.count(ledger)
    1

Recording what the time was for:

    iex> import Tempo.Sigils
    iex> arrangement = %Agenda.Arrangement{
    ...>   session: "Annual leave",
    ...>   interval: ~o"2026-09-14T09:00:00/2026-09-14T17:00:00",
    ...>   allocations: %{consultant: [Agenda.resource("Dana")]}
    ...> }
    iex> {:ok, ledger} =
    ...>   Agenda.Ledger.allocate(Agenda.Ledger.new(), arrangement, tag: {:leave, :annual})
    iex> ledger
    ...> |> Agenda.Ledger.to_list()
    ...> |> Enum.map(& &1.tag)
    [leave: :annual]

# `arrangements`

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

Rebuild what is allocated as arrangements, ready to pin.

This is the inverse of `allocate/2`, and it exists so that
re-arranging a programme can hold its published sessions still:
`Agenda.arrange/3` takes `t:Agenda.Arrangement.t/0` values as
`:pinned`, and these are the ones already booked.

An allocation records a resource by *name*, so the pool it came from
is needed to restore it. A name the pool does not have is an error
rather than a stand-in resource: a substitute would default to
concurrency 1 and no place, and would then quietly answer capacity
and travel questions wrongly.

### Arguments

* `ledger` is a `t:t/0`.

* `pool` is the list of `t:Agenda.Resource.t/0` the allocations
  were made from.

* `options` is a keyword list of options.

### Options

* `:only` is a session name, or a list of them, to rebuild. The
  default is every session in the ledger.

### Returns

* `{:ok, arrangements}` — one per session, ordered by session name,
  or

* `{:error, t:Agenda.Infeasible.t/0}` naming the resources the
  pool does not have.

### Examples

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.resource("Boardroom")
    iex> arrangement = %Agenda.Arrangement{
    ...>   session: "Review",
    ...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> {:ok, ledger} = Agenda.Ledger.allocate(Agenda.Ledger.new(), arrangement)
    iex> {:ok, [rebuilt]} = Agenda.Ledger.arrangements(ledger, [boardroom])
    iex> {rebuilt.session, Map.keys(rebuilt.allocations)}
    {"Review", [:room]}

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.resource("Boardroom")
    iex> arrangement = %Agenda.Arrangement{
    ...>   session: "Review",
    ...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> {:ok, ledger} = Agenda.Ledger.allocate(Agenda.Ledger.new(), arrangement)
    iex> {:error, reason} = Agenda.Ledger.arrangements(ledger, [])
    iex> Agenda.Infeasible.message(reason)
    "Review cannot be held: Boardroom is allocated but is not in the pool"

# `busy`

```elixir
@spec busy(t(), keyword()) :: %{optional(String.t()) =&gt; [Tempo.t()]}
```

What each resource is already claimed for, shaped for
`Agenda.Planner.plan/3`'s `:busy` option.

This is what closes the loop: plan against the ledger, allocate the
result, and the next plan sees it.

### Arguments

* `ledger` is a `t:t/0`.

* `options` is a keyword list of options.

### Options

* `:except` is a session name, or a list of them, whose allocations
  are ignored — use it when re-planning a session so it does not
  collide with the copy of itself it is about to replace, or when
  pinning sessions in `Agenda.arrange/3`, which claims what its
  pins hold and so must not be told about them twice.

### Returns

* a map of resource name to the intervals claiming it.

### Examples

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.resource("Boardroom")
    iex> arrangement = %Agenda.Arrangement{
    ...>   session: "Review",
    ...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> {:ok, ledger} = Agenda.Ledger.allocate(Agenda.Ledger.new(), arrangement)
    iex> Agenda.Ledger.busy(ledger)
    ...> |> Map.keys()
    ["Boardroom"]

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.resource("Boardroom")
    iex> arrangement = %Agenda.Arrangement{
    ...>   session: "Review",
    ...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> {:ok, ledger} = Agenda.Ledger.allocate(Agenda.Ledger.new(), arrangement)
    iex> Agenda.Ledger.busy(ledger, except: "Review")
    %{}

# `cancel`

```elixir
@spec cancel(t(), String.t()) :: {:ok, t()}
```

Cancel a session, freeing everything it holds, whether held or
booked.

The session-facing name for `release/2`, completing hold, confirm
and cancel: `release` says what happens to the resources, `cancel`
what happens to the session, as iCalendar's `STATUS:CANCELLED` does.

### Arguments

* `ledger` is a `t:t/0`.

* `session` is the session's name.

### Returns

* `{:ok, ledger}`. Cancelling a session that holds nothing is a
  no-op, so this is safe to call twice.

### Examples

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.resource("Boardroom")
    iex> arrangement = %Agenda.Arrangement{
    ...>   session: "Review",
    ...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> {:ok, ledger} = Agenda.Ledger.hold(Agenda.Ledger.new(), arrangement,
    ...>                   until: "2026-06-15T10:15:00")
    iex> {:ok, ledger} = Agenda.Ledger.cancel(ledger, "Review")
    iex> Agenda.Ledger.count(ledger)
    0

> *"Hold the boardroom for the review, then cancel the review."*

# `claim`

```elixir
@spec claim(t(), Agenda.Resource.t(), Agenda.Availability.pattern(), keyword()) ::
  {:ok, t()} | {:error, term()}
```

Book one resource over one interval, refusing what it cannot honour.

`record/4` records; `claim/4` **books**. The difference is the
difference between a timesheet and a schedule, and both are wanted:

* A schedule is a request about the future, so it must be checked. A
  consultant cannot be booked on a Saturday they do not work, nor
  twice over the same hour, and finding that out at the moment of
  booking is the point.

* A timesheet is a record of what happened, and the world is not
  obliged to match the contract. Somebody *did* work that Saturday.
  Refusing to write it down would leave the system unable to
  represent overtime, so `record/4` writes down anything and
  `Agenda.reconcile/3` is what reports the disagreement afterwards.

Both write to the same ledger, so a booking and a recorded hour
cannot double-claim the same person.

### Arguments

* `ledger` is a `t:t/0`.

* `resource` is the `t:Agenda.Resource.t/0` being claimed.

* `interval` is when — a Tempo value or an ISO 8601 string.

* `options` is a keyword list of options.

### Options

* `:tag` is what the claim is for, as a `t:Agenda.Allocation.tag/0`.

* `:requirement` is the requirement the resource is filling. The default is
  `:resource`.

* `:session` names the claim, and is what `release/2` takes. The
  default is the interval's ISO 8601 form, which is unique per
  resource per span.

### Returns

* `{:ok, ledger}`, or

* `{:error, reason}` — a sentence naming the time that could not be
  claimed and why, when the resource is not open then or is already
  claimed for part of it.

### Examples

    iex> import Tempo.Sigils
    iex> {:ok, dana} = Agenda.open(Agenda.resource("Dana"), "2026-08-10T09:00:00/2026-08-10T17:00:00")
    iex> {:ok, ledger} = Agenda.Ledger.claim(Agenda.Ledger.new(), dana, ~o"2026-08-10T09:00:00/2026-08-10T12:00:00")
    iex> Agenda.Ledger.count(ledger)
    1

Booking the same hour twice is refused rather than recorded:

    iex> import Tempo.Sigils
    iex> {:ok, dana} = Agenda.open(Agenda.resource("Dana"), "2026-08-10T09:00:00/2026-08-10T17:00:00")
    iex> {:ok, ledger} = Agenda.Ledger.claim(Agenda.Ledger.new(), dana, ~o"2026-08-10T09:00:00/2026-08-10T12:00:00")
    iex> {:error, reason} = Agenda.Ledger.claim(ledger, dana, ~o"2026-08-10T11:00:00/2026-08-10T13:00:00")
    iex> reason
    "Dana is already claimed for 2026Y8M10DT11H0M0S/T12H0M0S"

And so is time the resource does not work:

    iex> import Tempo.Sigils
    iex> {:ok, dana} = Agenda.open(Agenda.resource("Dana"), "2026-08-10T09:00:00/2026-08-10T17:00:00")
    iex> {:error, reason} = Agenda.Ledger.claim(Agenda.Ledger.new(), dana, ~o"2026-08-15T09:00:00/2026-08-15T12:00:00")
    iex> reason
    "Dana is not open for 2026Y8M15DT9H0M0S/T12H0M0S"

# `confirm`

```elixir
@spec confirm(t(), String.t()) :: {:ok, t()}
```

Turn a session's hold into a firm allocation.

### Arguments

* `ledger` is a `t:t/0`.

* `session` is the session's name.

### Returns

* `{:ok, ledger}`. Confirming a session that holds nothing, or one
  already firm, is a no-op.

### Examples

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.resource("Boardroom")
    iex> arrangement = %Agenda.Arrangement{
    ...>   session: "Review",
    ...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> {:ok, ledger} = Agenda.Ledger.hold(Agenda.Ledger.new(), arrangement,
    ...>                   until: "2026-06-15T10:15:00")
    iex> {:ok, ledger} = Agenda.Ledger.confirm(ledger, "Review")
    iex> Agenda.Ledger.holds(ledger)
    []

# `count`

```elixir
@spec count(t()) :: non_neg_integer()
```

How many allocations the ledger holds.

### Arguments

* `ledger` is a `t:t/0`.

### Returns

* the count.

### Examples

    iex> Agenda.Ledger.count(Agenda.Ledger.new())
    0

# `diff`

```elixir
@spec diff(t(), String.t(), Agenda.Arrangement.t()) :: [change()]
```

What would change if `session` moved to `arrangement`.

Only genuine movement is reported. A binding the new arrangement
still wants is `:keep`, never a `:release` followed by an
`:allocate` — that pair would hand the resource to a competing
booking in the gap between them, and churn rows that never moved.

### Arguments

* `ledger` is a `t:t/0`.

* `session` is the session's name.

* `arrangement` is the `t:Agenda.Arrangement.t/0` it should move
  to.

### Returns

* a list of `t:change/0`, ordered `:keep`, then `:release`, then
  `:allocate`.

### Examples

Re-planning a session to exactly where it already is changes
nothing:

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.resource("Boardroom")
    iex> arrangement = %Agenda.Arrangement{
    ...>   session: "Review",
    ...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> {:ok, ledger} = Agenda.Ledger.allocate(Agenda.Ledger.new(), arrangement)
    iex> Agenda.Ledger.diff(ledger, "Review", arrangement)
    ...> |> Enum.map(&elem(&1, 0))
    [:keep]

# `expire`

```elixir
@spec expire(t(), Agenda.Availability.pattern()) :: {:ok, t()} | {:error, term()}
```

Drop every hold that has lapsed by `now`.

This is the only function here that has anything to do with the
passing of time, and it takes the moment as an argument rather than
reading a clock. That is deliberate. `busy/2` is called inside
`Agenda.plan/3` and `Agenda.arrange/3`; if it consulted the
system clock, arranging the same programme twice would give
different answers as holds lapsed underneath it, and the ledger
would stop being a value you can reason about. Refusing to guess the
time is the same discipline as refusing to guess an unmeasured
journey.

A hold lapses when `now` has reached its expiry — a hold good
*until* ten past is gone at ten past.

### Arguments

* `ledger` is a `t:t/0`.

* `now` is the current moment — a Tempo value or an ISO 8601 string.

### Returns

* `{:ok, ledger}` with lapsed holds removed, or

* `{:error, reason}` when `now` cannot be read as a moment.

### Examples

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.resource("Boardroom")
    iex> arrangement = %Agenda.Arrangement{
    ...>   session: "Review",
    ...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> {:ok, ledger} = Agenda.Ledger.hold(Agenda.Ledger.new(), arrangement,
    ...>                   until: "2026-06-15T10:15:00")
    iex> {:ok, still_held} = Agenda.Ledger.expire(ledger, "2026-06-15T10:10:00")
    iex> Agenda.Ledger.count(still_held)
    1
    iex> {:ok, lapsed} = Agenda.Ledger.expire(ledger, "2026-06-15T10:15:00")
    iex> Agenda.Ledger.count(lapsed)
    0

# `for_session`

```elixir
@spec for_session(t(), String.t()) :: [Agenda.Allocation.t()]
```

What `session` currently holds.

### Arguments

* `ledger` is a `t:t/0`.

* `session` is the session's name.

### Returns

* the session's allocations, or `[]` when it holds nothing.

### Examples

    iex> Agenda.Ledger.for_session(Agenda.Ledger.new(), "Review")
    []

# `free_busy`

```elixir
@spec free_busy(t(), keyword()) :: %{
  optional(String.t()) =&gt; [{busy_type(), Tempo.t()}]
}
```

What each resource is already claimed for, and whether each claim is
firm or only held.

`busy/2` gives the intervals alone, as planning wants them. This
keeps the distinction iCalendar's free/busy draws: a booking is
`:busy`, a hold that has not been confirmed is `:tentative`. A
caller planning around somebody else's hold can see that it is one.

Time outside a resource's open hours, iCalendar's
`BUSY-UNAVAILABLE`, is not the ledger's to know: it is the resource's
own, and `Agenda.Availability.free/2` subtracts it.

### Arguments

* `ledger` is a `t:t/0`.

* `options` is a keyword list of options.

### Options

* `:except` is a session name, or a list of them, whose allocations
  are ignored, as for `busy/2`.

### Returns

* a map of resource name to `{busy_type, interval}` pairs, each
  resource's ordered by when they begin. See `t:busy_type/0`.

### Examples

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.resource("Boardroom")
    iex> review = %Agenda.Arrangement{
    ...>   session: "Review",
    ...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> retro = %Agenda.Arrangement{
    ...>   session: "Retro",
    ...>   interval: ~o"2026-06-16T14:00:00/2026-06-16T15:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> {:ok, ledger} = Agenda.Ledger.allocate(Agenda.Ledger.new(), review)
    iex> {:ok, ledger} = Agenda.Ledger.hold(ledger, retro, until: "2026-06-15T17:00:00")
    iex> ledger
    ...> |> Agenda.Ledger.free_busy()
    ...> |> Map.fetch!("Boardroom")
    ...> |> Enum.map(fn {type, _interval} -> type end)
    [:busy, :tentative]

> *"The boardroom is booked for the review and held for the retro."*

# `hold`

```elixir
@spec hold(t(), Agenda.Arrangement.t(), keyword()) :: {:ok, t()} | {:error, term()}
```

Claim resources *tentatively*, until `until`.

A hold is what a booking page takes while someone finds their card.
It occupies the resource exactly as an allocation does — that is the
point, and it is why holds live in the ledger rather than in a
persistence layer. Availability is derived on every call from what
the ledger holds, so a hold the ledger cannot see is a hold nobody
subtracts, and two people book the same room.

Nothing expires on its own. `expire/2` is what advances time, and
until it is called a hold is simply a claim. That keeps the ledger a
value: arranging the same programme twice gives the same answer,
because no function here reads a clock.

### Arguments

* `ledger` is a `t:t/0`.

* `arrangement` is a `t:Agenda.Arrangement.t/0`.

* `options` is a keyword list of options.

### Options

* `:until` is when the hold lapses — a Tempo value or an ISO 8601
  string. Required.

* `:tag` is what the claim was for, as a `t:Agenda.Allocation.tag/0`.
  A hold occupies a resource exactly as a booking does, so it carries
  the same tag. The default is `nil`.

### Returns

* `{:ok, ledger}`, or

* `{:error, reason}` when `:until` cannot be read as a moment.

### Examples

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.resource("Boardroom")
    iex> arrangement = %Agenda.Arrangement{
    ...>   session: "Review",
    ...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> {:ok, ledger} = Agenda.Ledger.hold(Agenda.Ledger.new(), arrangement,
    ...>                   until: "2026-06-15T10:15:00")
    iex> Agenda.Ledger.holds(ledger)
    ...> |> Enum.map(& &1.session)
    ["Review"]

# `holds`

```elixir
@spec holds(t()) :: [Agenda.Allocation.t()]
```

Every allocation that is still only a hold.

### Arguments

* `ledger` is a `t:t/0`.

### Returns

* the held allocations, ordered as `to_list/1` orders them.

### Examples

    iex> Agenda.Ledger.holds(Agenda.Ledger.new())
    []

# `new`

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

An empty ledger.

### Returns

* an empty `t:t/0`.

### Examples

    iex> Agenda.Ledger.count(Agenda.Ledger.new())
    0

# `record`

```elixir
@spec record(t(), Agenda.Resource.t(), Agenda.Availability.pattern(), keyword()) ::
  {:ok, t()} | {:error, term()}
```

Record one resource over one interval, checking nothing.

The unchecked half of the pair `claim/4` completes. Same arguments,
same options, same ledger — and no opinion about whether the time
was available.

That is not a weaker `claim/4`; it answers a different question. A
schedule is a request about the future and must be checked before it
is accepted. A timesheet is a record of what already happened, and
the world is not obliged to match the contract: somebody worked that
Saturday whether or not their contract says they work Saturdays, and
a system that refuses to write it down cannot represent overtime.

So `claim/4` refuses at the door and `record/4` writes it down —
leaving `Agenda.reconcile/3` to report the disagreement afterwards,
which is the only place that can weigh it against a whole period.

### Arguments

* `ledger` is a `t:t/0`.

* `resource` is the `t:Agenda.Resource.t/0` whose time is recorded.

* `interval` is when — a Tempo value or an ISO 8601 string.

* `options` is a keyword list of options.

### Options

* `:tag` is what the time was for, as a `t:Agenda.Allocation.tag/0`.

* `:requirement` is the requirement the resource filled. The default is
  `:resource`.

* `:session` names the record, and is what `release/2` takes. The
  default is the interval's ISO 8601 form, which is unique per
  resource per span.

### Returns

* `{:ok, ledger}`, or

* `{:error, reason}` when `interval` cannot be read as a span. Never
  for a reason of availability — that is `claim/4`'s business.

### Examples

Time outside the contract is recorded rather than refused:

    iex> import Tempo.Sigils
    iex> {:ok, dana} = Agenda.open(Agenda.resource("Dana"), "2026-08-10T09:00:00/2026-08-10T17:00:00")
    iex> {:error, _refused} = Agenda.Ledger.claim(Agenda.Ledger.new(), dana, ~o"2026-08-15T09:00:00/2026-08-15T12:00:00")
    iex> {:ok, ledger} = Agenda.Ledger.record(Agenda.Ledger.new(), dana, ~o"2026-08-15T09:00:00/2026-08-15T12:00:00")
    iex> Agenda.Ledger.count(ledger)
    1

And it is still a claim on the resource, so a booking cannot land on
top of it:

    iex> import Tempo.Sigils
    iex> {:ok, dana} = Agenda.open(Agenda.resource("Dana"), "2026-08-10T09:00:00/2026-08-10T17:00:00")
    iex> {:ok, ledger} = Agenda.Ledger.record(Agenda.Ledger.new(), dana, ~o"2026-08-10T09:00:00/2026-08-10T12:00:00")
    iex> {:error, reason} = Agenda.Ledger.claim(ledger, dana, ~o"2026-08-10T10:00:00/2026-08-10T11:00:00")
    iex> reason
    "Dana is already claimed for 2026Y8M10DT10H0M0S/T11H0M0S"

# `release`

```elixir
@spec release(t(), String.t()) :: {:ok, t()}
```

Free everything a session was holding.

### Arguments

* `ledger` is a `t:t/0`.

* `session` is the session's name.

### Returns

* `{:ok, ledger}`. Releasing a session that holds nothing is a no-op,
  so this is safe to call twice.

### Examples

    iex> import Tempo.Sigils
    iex> boardroom = Agenda.resource("Boardroom")
    iex> arrangement = %Agenda.Arrangement{
    ...>   session: "Review",
    ...>   interval: ~o"2026-06-16T10:00:00/2026-06-16T11:00:00",
    ...>   allocations: %{room: [boardroom]}
    ...> }
    iex> {:ok, ledger} = Agenda.Ledger.allocate(Agenda.Ledger.new(), arrangement)
    iex> {:ok, ledger} = Agenda.Ledger.release(ledger, "Review")
    iex> Agenda.Ledger.count(ledger)
    0

# `release_series`

```elixir
@spec release_series(t(), String.t(), keyword()) :: {:ok, t()} | {:error, term()}
```

Free everything held by a whole series.

The occurrences of a repeating session are separate sessions sharing
a series name, so cancelling the run is one call rather than a loop
the caller has to get right.

### Arguments

* `ledger` is a `t:t/0`.

* `series` is the series name — the original session's name, before
  it was expanded.

* `options` is a keyword list of options.

### Options

* `:from` releases only the occurrences starting at or after this
  point, a Tempo value or ISO 8601 string. Cancelling the rest of a
  term should not unpick the sessions already held.

### Returns

* `{:ok, ledger}`, or

* `{:error, reason}` when `:from` cannot be read.

### Examples

    iex> Agenda.Ledger.release_series(Agenda.Ledger.new(), "Stand-up")
    {:ok, Agenda.Ledger.new()}

# `to_list`

```elixir
@spec to_list(t()) :: [Agenda.Allocation.t()]
```

Every allocation in the ledger, whoever holds it.

### Arguments

* `ledger` is a `t:t/0`.

### Returns

* the allocations, ordered by session then requirement then resource so the
  listing is stable.

### Examples

    iex> Agenda.Ledger.to_list(Agenda.Ledger.new())
    []

---

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