WorkflowMergeCommandService.java
package org.hammer.audio.workflow.history;
import java.util.List;
import java.util.Objects;
import org.hammer.audio.workflow.Workflow;
import org.hammer.audio.workflow.dsl.WorkflowDslParser;
import org.hammer.audio.workflow.dsl.WorkflowDslSerializer;
import org.hammer.audio.workflow.merge.WorkflowMergeModels.Resolution;
import org.hammer.audio.workflow.merge.WorkflowMergeModels.Result;
import org.hammer.audio.workflow.merge.WorkflowThreeWayMerger;
import org.hammer.audio.workflow.store.CommitId;
import org.hammer.audio.workflow.store.CommitInfo;
import org.hammer.audio.workflow.store.CommitMetadata;
import org.hammer.audio.workflow.store.StaleWorkflowHeadException;
import org.hammer.audio.workflow.store.VersionedWorkflowStore;
import org.hammer.audio.workflow.store.WorkflowSnapshot;
/** Application service for exact stored-commit semantic merge preview and checkpoint creation. */
public final class WorkflowMergeCommandService {
private final VersionedWorkflowStore store;
private final WorkflowHistoryAccessPolicy accessPolicy;
private final WorkflowDslParser parser;
private final WorkflowDslSerializer serializer;
private final WorkflowThreeWayMerger merger;
/** Creates the production merge service over one repository-scoped workflow store. */
public WorkflowMergeCommandService(
VersionedWorkflowStore store, WorkflowHistoryAccessPolicy accessPolicy) {
this(
store,
accessPolicy,
new WorkflowDslParser(),
new WorkflowDslSerializer(),
new WorkflowThreeWayMerger());
}
WorkflowMergeCommandService(
VersionedWorkflowStore store,
WorkflowHistoryAccessPolicy accessPolicy,
WorkflowDslParser parser,
WorkflowDslSerializer serializer,
WorkflowThreeWayMerger merger) {
this.store = Objects.requireNonNull(store, "store");
this.accessPolicy = Objects.requireNonNull(accessPolicy, "accessPolicy");
this.parser = Objects.requireNonNull(parser, "parser");
this.serializer = Objects.requireNonNull(serializer, "serializer");
this.merger = Objects.requireNonNull(merger, "merger");
}
/** Loads exact stored commits and returns a deterministic semantic merge preview. */
public WorkflowMergePreview preview(PreviewWorkflowMergeCommand command) {
MergeContext context = loadContext(command);
accessPolicy.assertCompareAllowed(command.targetBranch(), context.base().id());
accessPolicy.assertCompareAllowed(command.remoteBranch(), context.base().id());
return new WorkflowMergePreview(
command.targetBranch(),
command.remoteBranch(),
command.baseCommit(),
command.localCommit(),
command.remoteCommit(),
context.base(),
context.local(),
context.remote(),
merger.preview(context.base(), context.local(), context.remote()));
}
/** Resolves all conflicts, validates the candidate and commits it on the expected target HEAD. */
public WorkflowMergeCommitResult resolveAndCommit(ResolveWorkflowMergeCommand command) {
Objects.requireNonNull(command, "command");
MergeContext context = loadContext(command.preview());
if (!command.expectedHead().equals(context.actualTargetHead())) {
throw new StaleWorkflowHeadException(
command.preview().targetBranch(), command.expectedHead(), context.actualTargetHead());
}
List<Resolution> resolutions =
command.resolutions().stream()
.map(
decision ->
new Resolution(
decision.conflictId(), decision.choice(), decision.customValue()))
.toList();
Result merged = merger.resolve(context.base(), context.local(), context.remote(), resolutions);
if (!merged.readyToCommit()) {
throw new WorkflowMergeRejectedException(
merged.unresolvedConflicts(), merged.validationViolations());
}
accessPolicy.assertMergeAllowed(command.preview().targetBranch(), merged.workflow().id());
CommitMetadata auditMetadata =
WorkflowMergeAuditTrail.metadata(
command.preview(), command.resolutions(), command.metadata());
WorkflowSnapshot snapshot =
new WorkflowSnapshot(merged.workflow().id(), serializer.serialize(merged.workflow()));
CommitId mergedCommit =
store.commitIfHead(
command.preview().targetBranch(), command.expectedHead(), snapshot, auditMetadata);
Workflow reloaded = parseAndValidate(store.loadAtCommit(mergedCommit));
if (!merged.workflow().equals(reloaded)) {
throw new IllegalStateException(
"Reloaded merge checkpoint differs from the deterministic resolved workflow");
}
return new WorkflowMergeCommitResult(
command.preview().targetBranch(),
command.preview().baseCommit(),
command.preview().localCommit(),
command.preview().remoteCommit(),
mergedCommit,
reloaded,
auditMetadata.message());
}
private MergeContext loadContext(PreviewWorkflowMergeCommand command) {
Objects.requireNonNull(command, "command");
List<CommitInfo> targetHistory = store.history(command.targetBranch(), Integer.MAX_VALUE);
List<CommitInfo> remoteHistory = store.history(command.remoteBranch(), Integer.MAX_VALUE);
if (targetHistory.isEmpty()) {
throw new IllegalArgumentException(
"Unknown or empty target workflow branch: " + command.targetBranch());
}
if (remoteHistory.isEmpty()) {
throw new IllegalArgumentException(
"Unknown or empty remote workflow branch: " + command.remoteBranch());
}
requireReachable(targetHistory, command.baseCommit(), command.targetBranch());
requireReachable(targetHistory, command.localCommit(), command.targetBranch());
requireReachable(remoteHistory, command.baseCommit(), command.remoteBranch());
requireReachable(remoteHistory, command.remoteCommit(), command.remoteBranch());
Workflow base = loadWorkflow(command.baseCommit());
Workflow local = loadWorkflow(command.localCommit());
Workflow remote = loadWorkflow(command.remoteCommit());
requireSameWorkflow(base, local, remote);
return new MergeContext(base, local, remote, targetHistory.getFirst().commitId());
}
private Workflow loadWorkflow(CommitId commitId) {
return parseAndValidate(store.loadAtCommit(commitId));
}
private Workflow parseAndValidate(WorkflowSnapshot snapshot) {
Workflow workflow = parser.parse(snapshot.dslText());
if (!snapshot.workflowId().equals(workflow.id())) {
throw new IllegalArgumentException(
"snapshot workflowId '"
+ snapshot.workflowId()
+ "' does not match DSL workflow id '"
+ workflow.id()
+ "'");
}
return workflow;
}
private static void requireSameWorkflow(Workflow base, Workflow local, Workflow remote) {
if (!base.id().equals(local.id()) || !base.id().equals(remote.id())) {
throw new IllegalArgumentException(
"Cannot merge commits with different workflow ids: "
+ base.id()
+ ", "
+ local.id()
+ " and "
+ remote.id());
}
}
private static void requireReachable(List<CommitInfo> history, CommitId commitId, String branch) {
boolean found = history.stream().anyMatch(info -> info.commitId().equals(commitId));
if (!found) {
throw new IllegalArgumentException(
"Commit " + commitId.value() + " is not reachable from branch " + branch);
}
}
private record MergeContext(
Workflow base, Workflow local, Workflow remote, CommitId actualTargetHead) {
MergeContext {
Objects.requireNonNull(base, "base");
Objects.requireNonNull(local, "local");
Objects.requireNonNull(remote, "remote");
Objects.requireNonNull(actualTargetHead, "actualTargetHead");
}
}
}