Pure mapper from canonical delivery facts to a curated workflow-facing outcome vocabulary used by Phase 25 progression rules.
The mapper has no IO and no Repo access. Callers preload the prior
Chimeway.Delivery row (and optionally the latest Chimeway.DeliveryAttempt)
inside the same progression transaction and pass them in. This keeps branch
semantics deterministic, replay-safe, and explainable from durable rows alone
per D-12.
Curated vocabulary (D-04)
from_delivery/2 returns either :not_branchable_yet or a three-tuple
{:branchable, outcome, evidence} where outcome is one of:
:delivered—delivery.status == :succeeded:suppressed—delivery.status == :suppressed:temporary_failure—delivery.status == :failed:retries_exhausted—delivery.status == :cancelledand`suppression_reason == "retries_exhausted"`:permanent_failure—delivery.status == :cancelledand`suppression_reason == "permanent_failure"`:bounced—delivery.status == :cancelledand`suppression_reason == "bounced"`
Early-fire warning for temporary_failure (WR-02)
temporary_failure resolves from a delivery.status == :failed row, which
is NOT terminal: Chimeway.Deliveries's @allowed_transitions permits
failed: [:dispatched], the path Oban uses while a delivery is being
retried. A workflow authoring:
%{"kind" => "on_outcome", "outcome" => "temporary_failure", "to_step" => "email"}fires to_step on the FIRST transient :failed write — before any
retry has been attempted. The original delivery may still succeed on a
later attempt, leaving the host with BOTH a successful primary delivery
AND the destination-step delivery for the same notification.
If the intent is "fire after retries are exhausted", use
retries_exhausted (which resolves only from a guaranteed-terminal
:cancelled row with suppression_reason == "retries_exhausted"). If the
intent IS "fire immediately on the first failure so we can try a
different channel while the original retries", pair the destination
step's notifier with an idempotency key so the host can collapse a
primary success + an early-fire escalation to one user-visible delivery.
See Chimeway.Notifier moduledoc near @progress_outcomes for the
authoring-time version of this warning.
Per D-05, :pending, :dispatched, and :digested deliveries always return
:not_branchable_yet. Cancelled rows whose suppression_reason is not in the
curated set also return :not_branchable_yet so workflow rules never advance
on a meaning the contract did not explicitly assign.
Evidence
The third tuple element carries replay-safe primitive evidence so workflow transitions can persist a sufficient explanation of why the outcome was chosen without any callback re-entry:
:delivery_status— string copy ofdelivery.status:suppression_reason— string copy ofdelivery.suppression_reason(may be `nil`):attempt_outcome— string copy of latest attempt outcome (ornil):attempt_error_class— string copy of latest attempt error class(or `nil`)
Summary
Types
@type outcome() ::
:delivered
| :suppressed
| :temporary_failure
| :retries_exhausted
| :permanent_failure
| :bounced
Functions
@spec from_delivery(Chimeway.Delivery.t(), Chimeway.DeliveryAttempt.t() | nil) :: result()