WorkflowDiff.java
package org.hammer.audio.workflow.history;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.hammer.audio.workflow.Edge;
import org.hammer.audio.workflow.Metadata;
import org.hammer.audio.workflow.Node;
import org.hammer.audio.workflow.Workflow;
import org.hammer.audio.workflow.WorkflowSemanticValueFormatter;
import org.hammer.audio.workflow.history.WorkflowChange.ElementKind;
/**
* Semantic diff between two {@link Workflow} snapshots.
*
* <p>A {@code WorkflowDiff} holds the ordered list of {@link WorkflowChange} atoms that represent
* the domain-level difference between a <em>before</em> and an <em>after</em> snapshot. Changes are
* based on stable workflow, node and edge identifiers rather than serialized line positions.
*
* <p>Owned by the semantic-analysis/history-projection layer. Must not depend on UI, JGit or
* execution internals.
*
* @param changes ordered, immutable list of semantic changes; empty when the two snapshots are
* semantically equivalent
*/
public record WorkflowDiff(List<WorkflowChange> changes) {
public WorkflowDiff {
changes = List.copyOf(Objects.requireNonNull(changes, "changes"));
}
/** Returns {@code true} when the two snapshots were semantically equivalent. */
public boolean isEmpty() {
return changes.isEmpty();
}
/**
* Computes the complete semantic diff between two workflow snapshots.
*
* <p>Whole-object additions/removals retain the established change variants. Metadata changes on
* existing nodes and edges retain {@link WorkflowChange.ParameterChanged} for transport
* compatibility. Workflow name/metadata, node type/label/ports and edge endpoints use {@link
* WorkflowChange.FieldChanged} with canonical values.
*
* @param before workflow snapshot before the change
* @param after workflow snapshot after the change
* @return deterministic domain-level changes
*/
public static WorkflowDiff compute(Workflow before, Workflow after) {
Objects.requireNonNull(before, "before");
Objects.requireNonNull(after, "after");
Map<String, Node> beforeNodes = indexNodes(before);
Map<String, Node> afterNodes = indexNodes(after);
Map<String, Edge> beforeEdges = indexEdges(before);
Map<String, Edge> afterEdges = indexEdges(after);
List<WorkflowChange> changes = new ArrayList<>();
collectRemovedEdges(beforeEdges, afterEdges, changes);
collectRemovedNodes(beforeNodes, afterNodes, changes);
collectAddedNodes(beforeNodes, afterNodes, changes);
collectAddedEdges(beforeEdges, afterEdges, changes);
collectWorkflowFieldChanges(before, after, changes);
collectNodeFieldChanges(beforeNodes, afterNodes, changes);
collectEdgeFieldChanges(beforeEdges, afterEdges, changes);
return new WorkflowDiff(changes);
}
private static void collectRemovedEdges(
Map<String, Edge> beforeEdges, Map<String, Edge> afterEdges, List<WorkflowChange> changes) {
for (Map.Entry<String, Edge> entry : beforeEdges.entrySet()) {
if (!afterEdges.containsKey(entry.getKey())) {
changes.add(new WorkflowChange.EdgeRemoved(entry.getValue()));
}
}
}
private static void collectRemovedNodes(
Map<String, Node> beforeNodes, Map<String, Node> afterNodes, List<WorkflowChange> changes) {
for (Map.Entry<String, Node> entry : beforeNodes.entrySet()) {
if (!afterNodes.containsKey(entry.getKey())) {
changes.add(new WorkflowChange.NodeRemoved(entry.getValue()));
}
}
}
private static void collectAddedNodes(
Map<String, Node> beforeNodes, Map<String, Node> afterNodes, List<WorkflowChange> changes) {
for (Map.Entry<String, Node> entry : afterNodes.entrySet()) {
if (!beforeNodes.containsKey(entry.getKey())) {
changes.add(new WorkflowChange.NodeAdded(entry.getValue()));
}
}
}
private static void collectAddedEdges(
Map<String, Edge> beforeEdges, Map<String, Edge> afterEdges, List<WorkflowChange> changes) {
for (Map.Entry<String, Edge> entry : afterEdges.entrySet()) {
if (!beforeEdges.containsKey(entry.getKey())) {
changes.add(new WorkflowChange.EdgeAdded(entry.getValue()));
}
}
}
private static void collectWorkflowFieldChanges(
Workflow before, Workflow after, List<WorkflowChange> changes) {
addFieldChange(changes, ElementKind.WORKFLOW, before.id(), "name", before.name(), after.name());
diffWorkflowMetadata(before.id(), before.metadata(), after.metadata(), changes);
}
private static void collectNodeFieldChanges(
Map<String, Node> beforeNodes, Map<String, Node> afterNodes, List<WorkflowChange> changes) {
for (Map.Entry<String, Node> entry : afterNodes.entrySet()) {
Node before = beforeNodes.get(entry.getKey());
if (before == null) {
continue;
}
Node after = entry.getValue();
addFieldChange(changes, ElementKind.NODE, before.id(), "type", before.type(), after.type());
addFieldChange(
changes, ElementKind.NODE, before.id(), "label", before.label(), after.label());
addFieldChange(
changes,
ElementKind.NODE,
before.id(),
"inputPorts",
WorkflowSemanticValueFormatter.ports(before.inputPorts()),
WorkflowSemanticValueFormatter.ports(after.inputPorts()));
addFieldChange(
changes,
ElementKind.NODE,
before.id(),
"outputPorts",
WorkflowSemanticValueFormatter.ports(before.outputPorts()),
WorkflowSemanticValueFormatter.ports(after.outputPorts()));
diffLegacyMetadata(
before.id(), before.metadata().entries(), after.metadata().entries(), changes);
}
}
private static void collectEdgeFieldChanges(
Map<String, Edge> beforeEdges, Map<String, Edge> afterEdges, List<WorkflowChange> changes) {
for (Map.Entry<String, Edge> entry : afterEdges.entrySet()) {
Edge before = beforeEdges.get(entry.getKey());
if (before == null) {
continue;
}
Edge after = entry.getValue();
addFieldChange(
changes,
ElementKind.EDGE,
before.id(),
"endpoints",
WorkflowSemanticValueFormatter.endpoints(before),
WorkflowSemanticValueFormatter.endpoints(after));
diffLegacyMetadata(
before.id(), before.metadata().entries(), after.metadata().entries(), changes);
}
}
private static void diffWorkflowMetadata(
String workflowId, Metadata before, Metadata after, List<WorkflowChange> changes) {
for (String key : sortedKeys(before.entries(), after.entries())) {
addFieldChange(
changes,
ElementKind.WORKFLOW,
workflowId,
"metadata." + key,
before.entries().get(key),
after.entries().get(key));
}
}
private static void diffLegacyMetadata(
String targetId,
Map<String, String> before,
Map<String, String> after,
List<WorkflowChange> changes) {
for (String key : sortedKeys(before, after)) {
String beforeValue = before.get(key);
String afterValue = after.get(key);
if (!Objects.equals(beforeValue, afterValue)) {
changes.add(new WorkflowChange.ParameterChanged(targetId, key, beforeValue, afterValue));
}
}
}
private static void addFieldChange(
List<WorkflowChange> changes,
ElementKind kind,
String targetId,
String fieldPath,
String before,
String after) {
if (!Objects.equals(before, after)) {
changes.add(new WorkflowChange.FieldChanged(kind, targetId, fieldPath, before, after));
}
}
private static List<String> sortedKeys(Map<String, String> before, Map<String, String> after) {
Set<String> keys = new TreeSet<>(before.keySet());
keys.addAll(after.keySet());
return List.copyOf(keys);
}
private static Map<String, Node> indexNodes(Workflow workflow) {
Map<String, Node> index = new TreeMap<>();
for (Node node : workflow.nodes()) {
if (index.putIfAbsent(node.id(), node) != null) {
throw new IllegalArgumentException("Duplicate node id: " + node.id());
}
}
return index;
}
private static Map<String, Edge> indexEdges(Workflow workflow) {
Map<String, Edge> index = new TreeMap<>();
for (Edge edge : workflow.edges()) {
if (index.putIfAbsent(edge.id(), edge) != null) {
throw new IllegalArgumentException("Duplicate edge id: " + edge.id());
}
}
return index;
}
}