SnapshotExecutionService.java

package org.hammer.audio.workflow.execution;

import java.time.Instant;
import java.util.Objects;
import org.hammer.audio.workflow.Workflow;
import org.hammer.audio.workflow.dsl.WorkflowDslParser;
import org.hammer.audio.workflow.store.CommitId;
import org.hammer.audio.workflow.store.CommitInfo;
import org.hammer.audio.workflow.store.VersionedWorkflowStore;
import org.hammer.audio.workflow.store.WorkflowSnapshot;

/**
 * Application service for creating {@link ExecutionSnapshot} objects from stored workflow
 * checkpoints and driving a complete dry-run execution pipeline.
 *
 * <p>This service bridges the persistence layer ({@link VersionedWorkflowStore}) and the execution
 * model ({@link ExecutionSnapshot}, {@link ExecutionPlan}, {@link ExecutionContext}, {@link
 * ExecutionResult}). It is the entry point for issue #211: <em>Create ExecutionSnapshot from stored
 * experiment graph</em>.
 *
 * <p>The {@link #run} method performs a <em>dry-run</em> execution: all nodes are transitioned from
 * {@link ExecutionStatus#IDLE} through {@link ExecutionStatus#RUNNING} to {@link
 * ExecutionStatus#COMPLETED}. No actual node computation takes place. This approach satisfies the
 * acceptance criteria that execution starts from a stable stored snapshot and that running
 * execution is isolated from ongoing edits.
 *
 * <p><strong>Dependency rules</strong>: this class lives in the {@code workflow.execution} package
 * and may import from {@code org.hammer.audio.workflow} (including sub-packages) and Java SE only.
 * It must not depend on UI, Swing, JGit, Hibernate or recording packages.
 *
 * <p><strong>Thread safety</strong>: not thread-safe. Concurrent access must be serialised by the
 * caller.
 */
public final class SnapshotExecutionService {

  private final VersionedWorkflowStore store;
  private final WorkflowDslParser parser;

  /**
   * Creates a service backed by the given workflow store and DSL parser.
   *
   * @param store versioned workflow checkpoint store
   * @param parser DSL parser for deserialising stored workflows
   */
  public SnapshotExecutionService(VersionedWorkflowStore store, WorkflowDslParser parser) {
    this.store = Objects.requireNonNull(store, "store");
    this.parser = Objects.requireNonNull(parser, "parser");
  }

  /**
   * Creates an {@link ExecutionSnapshot} from the workflow stored at the given commit.
   *
   * <p>The stored DSL text is parsed back into the workflow domain model and then frozen into an
   * immutable snapshot. Subsequent edits to the stored workflow have no effect on the returned
   * snapshot.
   *
   * @param commitId identifier of the stored workflow checkpoint
   * @param snapshotId stable identifier for the resulting snapshot
   * @param createdAt instant at which the snapshot is taken
   * @return immutable snapshot of the workflow at the given commit
   * @throws java.util.NoSuchElementException if the commit does not exist in the store
   * @throws org.hammer.audio.workflow.dsl.WorkflowDslParseException if the stored DSL is malformed
   */
  public ExecutionSnapshot snapshotAtCommit(
      CommitId commitId, String snapshotId, Instant createdAt) {
    Objects.requireNonNull(commitId, "commitId");
    Objects.requireNonNull(snapshotId, "snapshotId");
    Objects.requireNonNull(createdAt, "createdAt");
    WorkflowSnapshot stored = store.loadAtCommit(commitId);
    Workflow workflow = parser.parse(stored.dslText());
    return ExecutionSnapshot.of(snapshotId, workflow, createdAt);
  }

  /**
   * Runs a dry-run execution of the given snapshot and returns a {@link ReproducibilityBundle}.
   *
   * <p>The method derives an {@link ExecutionPlan} from the snapshot (topological sort), creates an
   * {@link ExecutionContext} and transitions every node through {@link ExecutionStatus#RUNNING} to
   * {@link ExecutionStatus#COMPLETED}. The resulting {@link ExecutionResult} is bundled together
   * with the snapshot and the optional commit provenance into an immutable {@link
   * ReproducibilityBundle}.
   *
   * <p>The {@code commitId} and {@code commitInfo} parameters may be {@code null} when the snapshot
   * was created outside of version control (e.g. from the live editor state during testing).
   *
   * @param snapshot the workflow snapshot to execute
   * @param planId stable identifier for the execution plan
   * @param executionId stable identifier for this execution run
   * @param commitId version-control identifier of the stored checkpoint (may be {@code null})
   * @param commitInfo author, message and timestamp of the stored checkpoint (may be {@code null})
   * @param startedAt instant at which the execution started
   * @param completedAt instant at which the execution finished
   * @return reproducibility bundle capturing the snapshot, result and optional provenance
   * @throws IllegalArgumentException if the workflow graph contains a cycle
   */
  public ReproducibilityBundle run(
      ExecutionSnapshot snapshot,
      String planId,
      String executionId,
      CommitId commitId,
      CommitInfo commitInfo,
      Instant startedAt,
      Instant completedAt) {
    Objects.requireNonNull(snapshot, "snapshot");
    StableExecutionIds.requireStable(planId, "planId");
    StableExecutionIds.requireStable(executionId, "executionId");
    Objects.requireNonNull(startedAt, "startedAt");
    Objects.requireNonNull(completedAt, "completedAt");

    ExecutionPlan executionPlan = ExecutionPlan.of(planId, snapshot);
    ExecutionContext context = new ExecutionContext(executionId, executionPlan, startedAt);

    for (String nodeId : executionPlan.orderedNodeIds()) {
      context.updateNodeStatus(nodeId, ExecutionStatus.RUNNING);
      context.updateNodeStatus(nodeId, ExecutionStatus.COMPLETED);
    }

    ExecutionResult result = context.toResult(completedAt);
    return new ReproducibilityBundle(snapshot, result, commitId, commitInfo);
  }
}