WorkflowMergeModels.java

package org.hammer.audio.workflow.merge;

import java.io.Serial;
import java.io.Serializable;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.hammer.audio.workflow.Workflow;

/** Framework-independent contracts for deterministic three-way workflow merge. */
public interface WorkflowMergeModels {

  /** Semantic workflow element owning a conflicted value. */
  enum ElementKind {
    WORKFLOW,
    NODE,
    EDGE
  }

  /** Typed semantic conflict classification. */
  enum ConflictKind {
    DIVERGENT_VALUE,
    STABLE_ID_COLLISION,
    DELETE_MODIFY,
    DELETE_CONNECT,
    DIVERGENT_EDGE_ENDPOINTS
  }

  /** Explicit user choice resolving one semantic conflict. */
  enum ResolutionChoice {
    BASE,
    LOCAL,
    REMOTE,
    DELETE,
    CUSTOM
  }

  /**
   * One ordered semantic merge conflict.
   *
   * @param conflictId deterministic identity derived from semantic location and kind
   * @param kind typed conflict classification
   * @param elementKind kind of owning workflow element
   * @param elementId stable workflow, node or edge identifier
   * @param fieldPath stable semantic field path
   * @param baseValue canonical base value, if present
   * @param localValue canonical local value, if present
   * @param remoteValue canonical remote value, if present
   * @param allowedChoices immutable supported resolution choices
   */
  record Conflict(
      String conflictId,
      ConflictKind kind,
      ElementKind elementKind,
      String elementId,
      String fieldPath,
      String baseValue,
      String localValue,
      String remoteValue,
      Set<ResolutionChoice> allowedChoices)
      implements Serializable {

    @Serial private static final long serialVersionUID = 1L;

    /** Stable ordering used by APIs, tests and user interfaces. */
    public static final Comparator<Conflict> ORDERING =
        Comparator.comparing(Conflict::elementKind)
            .thenComparing(Conflict::elementId)
            .thenComparing(Conflict::fieldPath)
            .thenComparing(Conflict::kind)
            .thenComparing(Conflict::conflictId);

    public Conflict {
      if (conflictId == null || conflictId.isBlank()) {
        throw new IllegalArgumentException("conflictId must not be blank");
      }
      Objects.requireNonNull(kind, "kind");
      Objects.requireNonNull(elementKind, "elementKind");
      if (elementId == null || elementId.isBlank()) {
        throw new IllegalArgumentException("elementId must not be blank");
      }
      if (fieldPath == null || fieldPath.isBlank()) {
        throw new IllegalArgumentException("fieldPath must not be blank");
      }
      allowedChoices = Set.copyOf(Objects.requireNonNull(allowedChoices, "allowedChoices"));
      if (allowedChoices.isEmpty()) {
        throw new IllegalArgumentException("allowedChoices must not be empty");
      }
    }
  }

  /**
   * One explicit conflict resolution.
   *
   * @param conflictId exact conflict identity returned by the preview
   * @param choice selected resolution choice
   * @param customValue explicit merged scalar value for {@link ResolutionChoice#CUSTOM}
   */
  record Resolution(String conflictId, ResolutionChoice choice, String customValue) {
    public Resolution {
      if (conflictId == null || conflictId.isBlank()) {
        throw new IllegalArgumentException("conflictId must not be blank");
      }
      Objects.requireNonNull(choice, "choice");
      if (choice == ResolutionChoice.CUSTOM) {
        Objects.requireNonNull(customValue, "customValue");
      } else if (customValue != null) {
        throw new IllegalArgumentException("customValue is only valid for CUSTOM resolutions");
      }
    }

    /** Creates a side- or delete-based resolution without a custom scalar value. */
    public Resolution(String conflictId, ResolutionChoice choice) {
      this(conflictId, choice, null);
    }
  }

  /**
   * Initial deterministic merge preview.
   *
   * @param autoMergedWorkflow workflow containing every non-conflicting automatic decision
   * @param conflicts unresolved semantic conflicts in stable order
   * @param validationViolations structural violations caused by the combined automatic decisions
   */
  record Preview(
      Workflow autoMergedWorkflow, List<Conflict> conflicts, List<String> validationViolations) {
    public Preview {
      Objects.requireNonNull(autoMergedWorkflow, "autoMergedWorkflow");
      conflicts = List.copyOf(Objects.requireNonNull(conflicts, "conflicts"));
      validationViolations =
          List.copyOf(Objects.requireNonNull(validationViolations, "validationViolations"));
      if (!conflicts.equals(conflicts.stream().sorted(Conflict.ORDERING).toList())) {
        throw new IllegalArgumentException("conflicts must use stable semantic ordering");
      }
    }

    /** Returns whether no explicit field resolution remains. */
    public boolean conflictFree() {
      return conflicts.isEmpty();
    }

    /** Returns whether the automatic result may be serialized and committed directly. */
    public boolean readyToCommit() {
      return conflicts.isEmpty() && validationViolations.isEmpty();
    }
  }

  /**
   * Result after applying zero or more explicit resolutions.
   *
   * @param workflow deterministic candidate workflow
   * @param unresolvedConflicts conflicts without a valid explicit decision
   * @param validationViolations structural workflow validation diagnostics
   */
  record Result(
      Workflow workflow, List<Conflict> unresolvedConflicts, List<String> validationViolations) {
    public Result {
      Objects.requireNonNull(workflow, "workflow");
      unresolvedConflicts =
          List.copyOf(Objects.requireNonNull(unresolvedConflicts, "unresolvedConflicts"));
      validationViolations =
          List.copyOf(Objects.requireNonNull(validationViolations, "validationViolations"));
    }

    /** Returns whether the candidate may be serialized and committed. */
    public boolean readyToCommit() {
      return unresolvedConflicts.isEmpty() && validationViolations.isEmpty();
    }
  }
}