# `Tayori.Digest`
[🔗](https://github.com/hattoriai/tayori/blob/v0.1.0/lib/tayori/digest.ex#L1)

An entry made of the inbox's changes (`Tayori.Inbox`): the changes
waiting, for the public, put in an entry for the people who use the app.

    {:ok, entry} = Tayori.Digest.run()
    {:ok, entry} = Tayori.Digest.run(ids: ["0193…", "0194…"])

The entry is a draft for the admin to read and publish, unless the
`:digest` config says `publish: :publish`; the changes it takes are
then `"drafted"`, with the entry's id. A change for the team only
(`audience: "internal"`), or put aside, never goes into one.

## Written by the model, or plain

When the admin has set up AI (`Tayori.AI.configured?/0`), the model
writes the entry from the changes: a title, a summary and a body, in
the first language and the product's voice, the highlights first. It
gets each change's title, body, category and highlight, never its ref,
link or author, and never a change for the team. With
`digest: [translate: true]`, it also translates the entry into the
other languages. When the model fails, the digest is plain, and a
warning is logged.

A plain digest, without AI or with `ai: false`: one change makes an
entry of its own, its title and its body; several make an entry titled
with the day, with a section for each category, the highlighted first.

The entry's categories and segments are always the changes', not the
model's.

## When

The `:every` of the `:digest` config: `:manual`, when the admin asks;
`:ingest`, each time changes come (`after_ingest/1`); or a schedule,
which Tayori's scheduler keeps (`next_run/3`). A scheduled digest with
fewer changes than `:minimum` makes nothing, and its changes wait for
the next time. With the `:oban` option, it runs in a job of
`Tayori.Worker`; without, in a task. `status/0` says where it stands.

# `schedule`

```elixir
@type schedule() :: %{:every =&gt; term(), :at =&gt; Time.t(), optional(atom()) =&gt; term()}
```

A schedule: the `:every` and `:at` of the `:digest` or `:emails` config.

# `after_ingest`

```elixir
@spec after_ingest(non_neg_integer()) :: :ok
```

Tells the digest that changes came: with `every: :ingest`, a digest
runs when at least one is new. `Tayori.Inbox.ingest/0` calls it, and
so does whatever else adds changes in batches, with the count of new
ones.

# `due?`

```elixir
@spec due?(Tayori.Config.t(), DateTime.t()) :: boolean()
```

Whether the scheduled digest of the config is due at a time: its
`next_run/3` after the last one has come.

# `next_run`

```elixir
@spec next_run(schedule(), DateTime.t() | nil, DateTime.t()) :: DateTime.t() | nil
```

The time of the next scheduled run, after the last one (or after
`now`, when there was none), or nil when the schedule has no time:

  * `{:weekly, day}` - the first such day after the last run, at `:at`;
  * `:monthly` - the first day of the month after the last run, at `:at`;
  * `{:days, n}` - n days after the day of the last run, at `:at`;
  * `:manual`, `:ingest` (and the `:each` of `:emails`) - nil.

A time that is past is due: a run missed while the app was down is
made once, at the next look.

## Examples

    iex> schedule = %{every: {:weekly, :monday}, at: ~T[09:00:00]}
    iex> Tayori.Digest.next_run(schedule, ~U[2026-10-19 09:00:30Z], ~U[2026-10-20 12:00:00Z])
    ~U[2026-10-26 09:00:00Z]

    iex> schedule = %{every: :monthly, at: ~T[09:00:00]}
    iex> Tayori.Digest.next_run(schedule, nil, ~U[2026-10-20 12:00:00Z])
    ~U[2026-11-01 09:00:00Z]

    iex> Tayori.Digest.next_run(%{every: :manual, at: ~T[09:00:00]}, nil, ~U[2026-10-20 12:00:00Z])
    nil

# `run`

```elixir
@spec run(keyword()) ::
  {:ok, Tayori.Entry.t()} | {:error, :nothing | Ecto.Changeset.t()}
```

Makes an entry of the changes waiting. Options:

  * `:ids` - these changes only (still only the new, public ones).
  * `:since`, `:until` - the changes that happened in this time.
  * `:publish` - `:draft` or `:publish`, instead of the config's.
  * `:ai` - `false` for a plain digest, even with AI set up.
  * `:now` - the time of the digest (now).
  * `:author` - the entry's author, as the resolver's `author/1` gives it.

Returns `{:ok, entry}`, `{:error, :nothing}` when no change is
waiting, or `{:error, changeset}`.

# `status`

```elixir
@spec status() :: %{
  every: term(),
  next_run_at: DateTime.t() | nil,
  last_run_at: DateTime.t() | nil,
  pending: non_neg_integer()
}
```

Where the digest stands, for the admin: its `:every`, the time of the
next scheduled one (nil for `:manual` and `:ingest`), the last
scheduled one (nil before the first), and how many changes wait.

---

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