WorkflowMergeHttpAdapter.java
package org.hammer.audio.workflow.editor.http;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.hammer.audio.workflow.editor.WorkflowProjection;
import org.hammer.audio.workflow.history.PreviewWorkflowMergeCommand;
import org.hammer.audio.workflow.history.ResolveWorkflowMergeCommand;
import org.hammer.audio.workflow.history.WorkflowMergeCommandService;
import org.hammer.audio.workflow.history.WorkflowMergeCommitResult;
import org.hammer.audio.workflow.history.WorkflowMergePreview;
import org.hammer.audio.workflow.merge.WorkflowMergeModels.Conflict;
import org.hammer.audio.workflow.merge.WorkflowMergeModels.ResolutionChoice;
import org.hammer.audio.workflow.merge.WorkflowMergeResolution;
import org.hammer.audio.workflow.store.CommitId;
import org.hammer.audio.workflow.store.CommitMetadata;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** HTTP boundary for deterministic semantic merge preview and resolved checkpoint creation. */
@RestController
@RequestMapping("/workflow/history/merge")
@ConditionalOnProperty(name = "workbench.persistence.mode", havingValue = "hibernate")
public final class WorkflowMergeHttpAdapter {
private final WorkflowMergeCommandService service;
/** Creates the transport adapter over the framework-independent merge service. */
public WorkflowMergeHttpAdapter(WorkflowMergeCommandService service) {
this.service = Objects.requireNonNull(service, "service");
}
/** Loads exact base/local/remote commits and returns a deterministic semantic preview. */
@PostMapping("/preview")
public PreviewResponse preview(@Valid @RequestBody PreviewRequest request) {
return PreviewResponse.from(service.preview(request.toCommand()));
}
/** Applies explicit conflict decisions and commits the validated result on the expected HEAD. */
@PostMapping("/resolve")
public ResolveResponse resolve(@Valid @RequestBody ResolveRequest request) {
return ResolveResponse.from(service.resolveAndCommit(request.toCommand()));
}
/**
* Exact stored versions for one preview.
*
* @param targetBranch local branch and eventual checkpoint target
* @param remoteBranch remote source branch
* @param baseCommitId common base commit
* @param localCommitId exact local commit
* @param remoteCommitId exact remote commit
*/
public record PreviewRequest(
@NotBlank String targetBranch,
@NotBlank String remoteBranch,
@NotBlank String baseCommitId,
@NotBlank String localCommitId,
@NotBlank String remoteCommitId) {
PreviewWorkflowMergeCommand toCommand() {
return new PreviewWorkflowMergeCommand(
targetBranch,
remoteBranch,
new CommitId(baseCommitId),
new CommitId(localCommitId),
new CommitId(remoteCommitId));
}
}
/**
* One explicit semantic conflict decision.
*
* @param conflictId exact conflict identity returned by preview
* @param choice selected resolution choice
* @param customValue custom scalar value for {@code CUSTOM}
*/
public record ResolutionRequest(
@NotBlank String conflictId, @NotNull ResolutionChoice choice, String customValue) {
WorkflowMergeResolution toDecision() {
return new WorkflowMergeResolution(conflictId, choice, customValue);
}
}
/**
* Resolve-and-commit request with optimistic target-HEAD protection and audit metadata.
*
* @param targetBranch local target branch
* @param remoteBranch remote source branch
* @param baseCommitId common base commit
* @param localCommitId exact local commit
* @param remoteCommitId exact remote commit
* @param expectedHeadCommitId target HEAD expected by the client
* @param resolutions explicit conflict decisions
* @param author merge checkpoint author
* @param message merge checkpoint message
* @param timestamp merge checkpoint timestamp
*/
public record ResolveRequest(
@NotBlank String targetBranch,
@NotBlank String remoteBranch,
@NotBlank String baseCommitId,
@NotBlank String localCommitId,
@NotBlank String remoteCommitId,
@NotBlank String expectedHeadCommitId,
@NotNull List<@Valid ResolutionRequest> resolutions,
@NotBlank String author,
@NotBlank String message,
@NotNull Instant timestamp) {
public ResolveRequest {
resolutions = List.copyOf(Objects.requireNonNull(resolutions, "resolutions"));
}
ResolveWorkflowMergeCommand toCommand() {
PreviewWorkflowMergeCommand preview =
new PreviewRequest(
targetBranch, remoteBranch, baseCommitId, localCommitId, remoteCommitId)
.toCommand();
return new ResolveWorkflowMergeCommand(
preview,
new CommitId(expectedHeadCommitId),
resolutions.stream().map(ResolutionRequest::toDecision).toList(),
new CommitMetadata(author, message, timestamp));
}
}
/**
* Transport-safe semantic conflict.
*
* @param conflictId deterministic conflict identity
* @param kind typed conflict classification
* @param elementKind workflow, node or edge
* @param elementId stable element identity
* @param fieldPath semantic field path
* @param baseValue canonical base value
* @param localValue canonical local value
* @param remoteValue canonical remote value
* @param allowedChoices supported explicit decisions
*/
public record ConflictResponse(
String conflictId,
String kind,
String elementKind,
String elementId,
String fieldPath,
String baseValue,
String localValue,
String remoteValue,
Set<ResolutionChoice> allowedChoices) {
public ConflictResponse {
allowedChoices = Set.copyOf(Objects.requireNonNull(allowedChoices, "allowedChoices"));
}
static ConflictResponse from(Conflict conflict) {
return new ConflictResponse(
conflict.conflictId(),
conflict.kind().name(),
conflict.elementKind().name(),
conflict.elementId(),
conflict.fieldPath(),
conflict.baseValue(),
conflict.localValue(),
conflict.remoteValue(),
conflict.allowedChoices());
}
}
/**
* Complete graph states, automatic candidate, ordered conflicts and validation impact.
*
* @param targetBranch local branch and eventual checkpoint target
* @param remoteBranch remote source branch
* @param baseCommitId exact common base commit
* @param localCommitId exact local commit selected for the preview
* @param remoteCommitId exact remote commit selected for the preview
* @param base canonical graph projection at the base commit
* @param local canonical graph projection at the local commit
* @param remote canonical graph projection at the remote commit
* @param autoMerged canonical graph containing every automatic non-conflicting decision
* @param conflicts ordered unresolved semantic conflicts
* @param validationViolations structural violations in the automatic candidate
* @param readyToCommit whether the automatic candidate has no conflicts or violations
*/
public record PreviewResponse(
String targetBranch,
String remoteBranch,
String baseCommitId,
String localCommitId,
String remoteCommitId,
WorkflowProjection base,
WorkflowProjection local,
WorkflowProjection remote,
WorkflowProjection autoMerged,
List<ConflictResponse> conflicts,
List<String> validationViolations,
boolean readyToCommit) {
public PreviewResponse {
conflicts = List.copyOf(Objects.requireNonNull(conflicts, "conflicts"));
validationViolations =
List.copyOf(Objects.requireNonNull(validationViolations, "validationViolations"));
}
static PreviewResponse from(WorkflowMergePreview preview) {
return new PreviewResponse(
preview.targetBranch(),
preview.remoteBranch(),
preview.baseCommit().value(),
preview.localCommit().value(),
preview.remoteCommit().value(),
WorkflowProjection.fromWorkflow(preview.baseWorkflow()),
WorkflowProjection.fromWorkflow(preview.localWorkflow()),
WorkflowProjection.fromWorkflow(preview.remoteWorkflow()),
WorkflowProjection.fromWorkflow(preview.merge().autoMergedWorkflow()),
preview.merge().conflicts().stream().map(ConflictResponse::from).toList(),
preview.merge().validationViolations(),
preview.merge().readyToCommit());
}
}
/**
* Newly created merge checkpoint and exact reloaded workflow projection.
*
* @param targetBranch branch receiving the resolved checkpoint
* @param baseCommitId exact common base commit used by the merge
* @param localCommitId exact local commit protected as the expected target HEAD
* @param remoteCommitId exact remote commit merged into the target branch
* @param mergedCommitId newly created authoritative merge checkpoint
* @param workflow canonical graph reloaded from the merged commit
* @param auditMessage deterministic commit message including merge provenance and decisions
*/
public record ResolveResponse(
String targetBranch,
String baseCommitId,
String localCommitId,
String remoteCommitId,
String mergedCommitId,
WorkflowProjection workflow,
String auditMessage) {
static ResolveResponse from(WorkflowMergeCommitResult result) {
return new ResolveResponse(
result.targetBranch(),
result.baseCommit().value(),
result.localCommit().value(),
result.remoteCommit().value(),
result.mergedCommit().value(),
WorkflowProjection.fromWorkflow(result.workflow()),
result.auditMessage());
}
}
}