MergeConflictReport.java
package org.hammer.audio.workflow.history;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Conflict report produced by reconciling two {@link WorkflowDiff} objects that share a common base
* snapshot.
*
* <p>Conflicts are expressed as domain-level model objects (see {@link MergeConflict}), not as
* text-diff hunks.
*
* <p>Two conflict patterns are detected:
*
* <ul>
* <li><b>Delete-vs-modify</b>: one diff removes a node while the other diff modifies a property
* on the same node. Because the intent of each side is incompatible, neither can be
* auto-resolved.
* <li><b>Connect-vs-parameter-change</b>: one diff adds an edge while the other diff modifies a
* property on one of the edge's endpoint nodes. The edge may become invalid after the
* property change, so human review is required.
* </ul>
*
* <p>Use {@link #detect(WorkflowDiff, WorkflowDiff)} to create an instance. Both diffs must be
* derived from the same base {@link org.hammer.audio.workflow.Workflow} snapshot.
*
* <p>Owned by the semantic-analysis/history-projection layer. Must not depend on UI, JGit or
* execution internals.
*
* @param conflicts immutable list of detected conflicts; empty when the two diffs can be merged
* without conflict
*/
public record MergeConflictReport(List<MergeConflict> conflicts) {
public MergeConflictReport {
conflicts = List.copyOf(Objects.requireNonNull(conflicts, "conflicts"));
}
/** Returns {@code true} when at least one merge conflict was detected. */
public boolean hasConflicts() {
return !conflicts.isEmpty();
}
/**
* Detects conflicts between two diffs that were computed from the same base workflow snapshot.
*
* @param ours diff from base to the local branch tip; must not be {@code null}
* @param theirs diff from base to the remote branch tip; must not be {@code null}
* @return conflict report; never {@code null}
*/
public static MergeConflictReport detect(WorkflowDiff ours, WorkflowDiff theirs) {
Objects.requireNonNull(ours, "ours");
Objects.requireNonNull(theirs, "theirs");
Set<String> oursRemovedNodeIds = removedNodeIds(ours);
Set<String> theirsRemovedNodeIds = removedNodeIds(theirs);
Set<String> oursChangedTargetIds = changedParameterTargetIds(ours);
Set<String> theirsChangedTargetIds = changedParameterTargetIds(theirs);
List<MergeConflict> conflicts = new ArrayList<>();
detectDeleteVsModify(
oursRemovedNodeIds,
theirsChangedTargetIds,
theirsRemovedNodeIds,
oursChangedTargetIds,
conflicts);
detectConnectVsParameterChange(ours.changes(), theirsChangedTargetIds, conflicts);
detectConnectVsParameterChange(theirs.changes(), oursChangedTargetIds, conflicts);
return new MergeConflictReport(conflicts);
}
private static void detectDeleteVsModify(
Set<String> oursRemovedNodeIds,
Set<String> theirsChangedTargetIds,
Set<String> theirsRemovedNodeIds,
Set<String> oursChangedTargetIds,
List<MergeConflict> conflicts) {
for (String nodeId : oursRemovedNodeIds) {
if (theirsChangedTargetIds.contains(nodeId)) {
conflicts.add(new MergeConflict.DeleteVsModify(nodeId));
}
}
for (String nodeId : theirsRemovedNodeIds) {
if (oursChangedTargetIds.contains(nodeId)) {
conflicts.add(new MergeConflict.DeleteVsModify(nodeId));
}
}
}
private static void detectConnectVsParameterChange(
List<WorkflowChange> changes,
Set<String> otherChangedTargetIds,
List<MergeConflict> conflicts) {
for (WorkflowChange change : changes) {
if (change instanceof WorkflowChange.EdgeAdded added) {
String conflictingNodeId =
findChangedEndpoint(
added.edge().sourceNodeId(), added.edge().targetNodeId(), otherChangedTargetIds);
if (conflictingNodeId != null) {
conflicts.add(
new MergeConflict.ConnectVsParameterChange(added.edge().id(), conflictingNodeId));
}
}
}
}
private static String findChangedEndpoint(
String sourceNodeId, String targetNodeId, Set<String> changedTargetIds) {
if (changedTargetIds.contains(sourceNodeId)) {
return sourceNodeId;
}
if (changedTargetIds.contains(targetNodeId)) {
return targetNodeId;
}
return null;
}
private static Set<String> removedNodeIds(WorkflowDiff diff) {
Set<String> ids = new LinkedHashSet<>();
for (WorkflowChange change : diff.changes()) {
if (change instanceof WorkflowChange.NodeRemoved removed) {
ids.add(removed.node().id());
}
}
return ids;
}
private static Set<String> changedParameterTargetIds(WorkflowDiff diff) {
Set<String> ids = new LinkedHashSet<>();
for (WorkflowChange change : diff.changes()) {
if (change instanceof WorkflowChange.ParameterChanged changed) {
ids.add(changed.targetId());
}
}
return ids;
}
}