Durable workflow progression engine for Phase 25.
This is the single seam through which workflow runs move from active to
waiting and from waiting/active to the next step. It evaluates the active
step's normalized progress rules against the canonical prior delivery row
inside one transaction, persisting the decision durably so that:
wait_untilrules write the run to:waitingwith explicitstatus_reason: "waiting_for_step_progression"and astatus_contextmap carryingrule_kind,anchor,anchor_delivery_id,anchor_delivery_status,anchor_timestamp,due_at, andto_step(D-01/D-13).on_outcomerules append aworkflow_transitionwith reasonprogressed_on_delivery_outcomeand the curated workflow outcome plus raw evidence facts (D-12), then advance the workflow run cursor to the target step and emit the next-step delivery through the canonicalChimeway.DeliveryPlanning.plan_next_step_delivery/3seam (D-10).
Re-entry is duplicate-safe: if the run is no longer :active, if the prior
delivery is not converged yet, if the curated mapper returns
:not_branchable_yet, or if no progress rule matches, the engine returns
{:noop, run, reason} without creating any new delivery rows or appending
transitions. This is the ESC-03 contract.
All locking happens inside one Repo.transaction/1:
- the workflow run row is locked with
FOR UPDATEfirst - the active-step delivery row is then locked with
FOR UPDATE
Threats covered:
- T-25-04 (tampering): outcomes are derived from canonical persisted
rows only via
ProgressionOutcome.from_delivery/2; the engine never branches from queue or in-flight job state. - T-25-05 (repudiation): explicit
status_reasonandreasonstrings plus the curatedstatus_context/ transitioncontextkeys make the decision auditable from durable rows alone. - T-25-06 (DoS / duplicate emission): noop short-circuits prevent retry storms from emitting duplicate next-step deliveries.
Summary
Functions
Lists workflow runs that are currently :waiting with a due wait gate that
has elapsed (per persisted status_context["due_at"]) and re-evaluates each
one through progress_run/2. The Plan 25-03 due-step worker calls into
this helper so wait gates always advance through the same shared seam.
Evaluates the workflow run's active step against the canonical prior delivery row and persists the resulting waiting/advanced/noop outcome.
Types
@type progress_result() :: {:ok, {:advanced, Chimeway.Workflows.WorkflowRun.t(), [Chimeway.Delivery.t()]}} | {:ok, {:waiting, Chimeway.Workflows.WorkflowRun.t()}} | {:ok, {:completed, Chimeway.Workflows.WorkflowRun.t()}} | {:ok, {:stopped, Chimeway.Workflows.WorkflowRun.t()}} | {:ok, {:noop, Chimeway.Workflows.WorkflowRun.t() | nil, atom()}} | {:error, term()}
Functions
@spec progress_due_runs(keyword()) :: [progress_result()]
Lists workflow runs that are currently :waiting with a due wait gate that
has elapsed (per persisted status_context["due_at"]) and re-evaluates each
one through progress_run/2. The Plan 25-03 due-step worker calls into
this helper so wait gates always advance through the same shared seam.
@spec progress_run( Ecto.UUID.t(), keyword() ) :: progress_result()
Evaluates the workflow run's active step against the canonical prior delivery row and persists the resulting waiting/advanced/noop outcome.
Options:
:now—DateTime.t()used as the evaluation time for due-checks and anchor stamping. Defaults toDateTime.utc_now/0. Provided for deterministic tests and the due-step worker (Plan 25-03).