ExecutionResult.java

package org.hammer.audio.workflow.execution;

import java.time.Instant;
import java.util.Map;
import java.util.Objects;

/**
 * Immutable record of a completed workflow execution.
 *
 * <p>Produced by {@link ExecutionContext#toResult(Instant)} once all nodes have reached a terminal
 * status. The result captures a frozen copy of the per-node statuses; the originating context may
 * be discarded after this point.
 *
 * @param executionId stable identifier for this execution
 * @param planId identifier of the plan that was executed
 * @param nodeStatuses terminal status of each node, keyed by node identifier
 * @param startedAt instant at which execution started
 * @param completedAt instant at which execution finished
 */
public record ExecutionResult(
    String executionId,
    String planId,
    Map<String, ExecutionStatus> nodeStatuses,
    Instant startedAt,
    Instant completedAt) {

  public ExecutionResult {
    StableExecutionIds.requireStable(executionId, "executionId");
    StableExecutionIds.requireStable(planId, "planId");
    Objects.requireNonNull(nodeStatuses, "nodeStatuses");
    Objects.requireNonNull(startedAt, "startedAt");
    Objects.requireNonNull(completedAt, "completedAt");
    nodeStatuses = Map.copyOf(nodeStatuses);
  }

  /**
   * Returns the overall status of this execution.
   *
   * <ul>
   *   <li>{@link ExecutionStatus#FAILED} — if any node failed.
   *   <li>{@link ExecutionStatus#CANCELLED} — if any node was cancelled (and none failed).
   *   <li>{@link ExecutionStatus#COMPLETED} — if every node completed successfully.
   *   <li>{@link ExecutionStatus#SKIPPED} — if no node failed or was cancelled but not all
   *       completed (e.g. some were skipped).
   * </ul>
   */
  public ExecutionStatus overallStatus() {
    if (nodeStatuses.containsValue(ExecutionStatus.FAILED)) {
      return ExecutionStatus.FAILED;
    }
    if (nodeStatuses.containsValue(ExecutionStatus.CANCELLED)) {
      return ExecutionStatus.CANCELLED;
    }
    if (nodeStatuses.values().stream().allMatch(s -> s == ExecutionStatus.COMPLETED)) {
      return ExecutionStatus.COMPLETED;
    }
    return ExecutionStatus.SKIPPED;
  }
}