# `Chimeway.Traces.Explanation`
[🔗](https://github.com/jonlunsford/chimeway/blob/v1.0.0/lib/chimeway/traces/explanation.ex#L1)

Structured explanation of a single delivery — the primary operator debugging artifact.

Returned by `Chimeway.Traces.explain_delivery/1`. Contains the full context
needed to answer "why was this delivery suppressed/failed/succeeded?" without
requiring additional queries.

## Fields

- `delivery_id` — UUID of the delivery row
- `event_id` — UUID of the parent event
- `correlation_id` — host-app correlation string (request_id, trace_id), or nil
- `notification_key` — stable notification type identifier
- `recipient_id` — recipient identity string
- `channel` — delivery channel string (for example "in_app", "email", "webhook_partner")
- `render_key` — stable per-channel render identity persisted on the delivery row, or nil
- `render_version` — stable per-channel render version persisted on the delivery row, or nil
- `status` — final delivery status: :succeeded | :failed | :suppressed | :pending | :cancelled
- `planning_reason` — orchestration/planning reason when the delivery is intentionally held, else nil
- `planning_context` — sanitized persisted planning facts for explainability, else nil
- `next_eligible_at` — UTC timestamp for the next dispatchable moment when deferred, else nil
- `resume_source` — sanitized scheduler/source label when a deferred row later resumes, else nil
- `resume_scheduled_at` — original UTC time the deferred row was scheduled to resume, else nil
- `resumed_at` — UTC timestamp when the canonical delivery row left deferred state, else nil
- `suppression_reason` — reason atom string when status is `:suppressed` OR `:cancelled`,
  else nil. The four documented reason strings are:
    * `"channel_disabled"` — set when status is `:suppressed` (policy preference blocked the channel)
    * `"retries_exhausted"` — set when status is `:cancelled` (Oban exhausted max_attempts on transient failures, REL-03 D-10/D-11)
    * `"permanent_failure"` — set when status is `:cancelled` (adapter returned a permanent error)
    * `"bounced"` — set when status is `:cancelled` (adapter returned a bounce)
- `last_attempt` — map with :outcome, :inserted_at, :attempt_number, :error_class, :adapter_module for the most recent attempt, or nil. `:adapter_module` is nil for pre-Phase-29 attempts.
- `digest` — digest-specific reasoning for source or emitted digest rows, else nil
- `timeline` — chronological list of lifecycle events, each a map with :at, :event, :detail

# `t`

```elixir
@type t() :: %Chimeway.Traces.Explanation{
  channel: String.t(),
  correlation_id: String.t() | nil,
  delivery_id: String.t(),
  digest: map() | nil,
  event_id: String.t(),
  last_attempt:
    %{
      outcome: atom(),
      inserted_at: DateTime.t(),
      attempt_number: pos_integer() | nil,
      error_class: String.t() | nil,
      adapter_module: String.t() | nil
    }
    | nil,
  next_eligible_at: DateTime.t() | nil,
  notification_key: String.t(),
  planning_context: map() | nil,
  planning_reason: String.t() | nil,
  recipient_id: String.t(),
  render_key: String.t() | nil,
  render_version: pos_integer() | nil,
  resume_scheduled_at: DateTime.t() | nil,
  resume_source: String.t() | nil,
  resumed_at: DateTime.t() | nil,
  status:
    :succeeded | :failed | :suppressed | :pending | :cancelled | :dispatched,
  suppression_reason: String.t() | nil,
  timeline: [timeline_entry()]
}
```

# `timeline_entry`

```elixir
@type timeline_entry() :: %{at: DateTime.t(), event: atom(), detail: map()}
```

---

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