SemanticDiffDescriber.java
package com.taxonomy.dsl.diff;
import com.taxonomy.dsl.model.ArchitectureElement;
import com.taxonomy.dsl.model.ArchitectureRelation;
import java.util.*;
/**
* Converts a structural {@link ModelDiff} into a list of human-readable
* {@link SemanticChange} instances.
*
* <p>While {@code ModelDiff} answers <em>"what objects differ?"</em>,
* the describer answers <em>"what specifically changed and why does it matter?"</em>.
* This is the foundation for:
* <ul>
* <li>Reviewable architecture change logs</li>
* <li>Automated release notes</li>
* <li>Change impact summaries</li>
* <li>History search annotations</li>
* </ul>
*
* <p>This class is a pure library component — no Spring, no JPA.
*/
public class SemanticDiffDescriber {
/**
* Derive semantic changes from a model diff.
*
* @param diff the structural diff (must not be {@code null})
* @return an unmodifiable list of semantic changes, in deterministic order
*/
public List<SemanticChange> describe(ModelDiff diff) {
Objects.requireNonNull(diff, "diff must not be null");
List<SemanticChange> changes = new ArrayList<>();
describeAddedElements(diff, changes);
describeRemovedElements(diff, changes);
describeChangedElements(diff, changes);
describeAddedRelations(diff, changes);
describeRemovedRelations(diff, changes);
describeChangedRelations(diff, changes);
return Collections.unmodifiableList(changes);
}
/**
* Produce a compact text summary of semantic changes.
*
* @param diff the structural diff
* @return multi-line summary suitable for commit messages or change logs
*/
public String summarize(ModelDiff diff) {
List<SemanticChange> changes = describe(diff);
if (changes.isEmpty()) {
return "No changes detected.";
}
StringBuilder sb = new StringBuilder();
sb.append(changes.size()).append(" semantic change(s):\n");
Map<SemanticChangeType, Long> counts = new LinkedHashMap<>();
for (SemanticChange c : changes) {
counts.merge(c.changeType(), 1L, Long::sum);
}
for (Map.Entry<SemanticChangeType, Long> entry : counts.entrySet()) {
sb.append(" - ").append(entry.getKey().getLabel())
.append(": ").append(entry.getValue()).append('\n');
}
return sb.toString();
}
// ── Added elements ───────────────────────────────────────────────
private void describeAddedElements(ModelDiff diff, List<SemanticChange> out) {
for (ArchitectureElement e : diff.addedElements()) {
String desc = String.format("Added %s '%s' (%s)",
safe(e.getType()), safe(e.getTitle()), safe(e.getId()));
out.add(SemanticChange.element(SemanticChangeType.ELEMENT_ADDED, e.getId(), desc));
}
}
// ── Removed elements ─────────────────────────────────────────────
private void describeRemovedElements(ModelDiff diff, List<SemanticChange> out) {
for (ArchitectureElement e : diff.removedElements()) {
String desc = String.format("Removed %s '%s' (%s)",
safe(e.getType()), safe(e.getTitle()), safe(e.getId()));
out.add(SemanticChange.element(SemanticChangeType.ELEMENT_REMOVED, e.getId(), desc));
}
}
// ── Changed elements ─────────────────────────────────────────────
private void describeChangedElements(ModelDiff diff, List<SemanticChange> out) {
for (ModelDiff.ElementChange ch : diff.changedElements()) {
ArchitectureElement before = ch.before();
ArchitectureElement after = ch.after();
if (!Objects.equals(before.getType(), after.getType())) {
out.add(SemanticChange.element(SemanticChangeType.ELEMENT_TYPE_CHANGED, after.getId(),
String.format("Type changed from '%s' to '%s' for %s",
safe(before.getType()), safe(after.getType()), safe(after.getId())),
before.getType(), after.getType()));
}
if (!Objects.equals(before.getTitle(), after.getTitle())) {
out.add(SemanticChange.element(SemanticChangeType.ELEMENT_TITLE_CHANGED, after.getId(),
String.format("Title changed from '%s' to '%s' for %s",
safe(before.getTitle()), safe(after.getTitle()), safe(after.getId())),
before.getTitle(), after.getTitle()));
}
if (!Objects.equals(before.getDescription(), after.getDescription())) {
out.add(SemanticChange.element(SemanticChangeType.ELEMENT_DESCRIPTION_CHANGED, after.getId(),
String.format("Description changed for %s", safe(after.getId())),
before.getDescription(), after.getDescription()));
}
if (!Objects.equals(before.getTaxonomy(), after.getTaxonomy())) {
out.add(SemanticChange.element(SemanticChangeType.ELEMENT_TAXONOMY_CHANGED, after.getId(),
String.format("Taxonomy changed from '%s' to '%s' for %s",
safe(before.getTaxonomy()), safe(after.getTaxonomy()), safe(after.getId())),
before.getTaxonomy(), after.getTaxonomy()));
}
if (!Objects.equals(before.getExtensions(), after.getExtensions())) {
out.add(SemanticChange.element(SemanticChangeType.ELEMENT_EXTENSIONS_CHANGED, after.getId(),
String.format("Extensions changed for %s", safe(after.getId()))));
}
}
}
// ── Added relations ──────────────────────────────────────────────
private void describeAddedRelations(ModelDiff diff, List<SemanticChange> out) {
for (ArchitectureRelation r : diff.addedRelations()) {
String key = relationKey(r);
String desc = String.format("Added relation %s → %s → %s",
safe(r.getSourceId()), safe(r.getRelationType()), safe(r.getTargetId()));
out.add(SemanticChange.relation(SemanticChangeType.RELATION_ADDED, key, desc));
}
}
// ── Removed relations ────────────────────────────────────────────
private void describeRemovedRelations(ModelDiff diff, List<SemanticChange> out) {
for (ArchitectureRelation r : diff.removedRelations()) {
String key = relationKey(r);
String desc = String.format("Removed relation %s → %s → %s",
safe(r.getSourceId()), safe(r.getRelationType()), safe(r.getTargetId()));
out.add(SemanticChange.relation(SemanticChangeType.RELATION_REMOVED, key, desc));
}
}
// ── Changed relations ────────────────────────────────────────────
private void describeChangedRelations(ModelDiff diff, List<SemanticChange> out) {
for (ModelDiff.RelationChange ch : diff.changedRelations()) {
ArchitectureRelation before = ch.before();
ArchitectureRelation after = ch.after();
String key = relationKey(after);
if (!Objects.equals(before.getStatus(), after.getStatus())) {
out.add(SemanticChange.relation(SemanticChangeType.RELATION_STATUS_CHANGED, key,
String.format("Status changed from '%s' to '%s' for %s",
safe(before.getStatus()), safe(after.getStatus()), key),
before.getStatus(), after.getStatus()));
}
if (!Objects.equals(before.getConfidence(), after.getConfidence())) {
out.add(SemanticChange.relation(SemanticChangeType.RELATION_CONFIDENCE_CHANGED, key,
String.format("Confidence changed from %s to %s for %s",
safe(str(before.getConfidence())), safe(str(after.getConfidence())), key),
str(before.getConfidence()), str(after.getConfidence())));
}
if (!Objects.equals(before.getProvenance(), after.getProvenance())) {
out.add(SemanticChange.relation(SemanticChangeType.RELATION_PROVENANCE_CHANGED, key,
String.format("Provenance changed from '%s' to '%s' for %s",
safe(before.getProvenance()), safe(after.getProvenance()), key),
before.getProvenance(), after.getProvenance()));
}
if (!Objects.equals(before.getExtensions(), after.getExtensions())) {
out.add(SemanticChange.relation(SemanticChangeType.RELATION_EXTENSIONS_CHANGED, key,
String.format("Extensions changed for %s", key)));
}
}
}
// ── Helpers ──────────────────────────────────────────────────────
private static String relationKey(ArchitectureRelation r) {
return r.getSourceId() + "|" + r.getRelationType() + "|" + r.getTargetId();
}
private static String safe(String value) {
return value != null ? value : "<none>";
}
private static String str(Double value) {
return value != null ? String.valueOf(value) : null;
}
}