RelationshipBuildStep.java
package com.taxonomy.architecture.pipeline;
import com.taxonomy.catalog.service.PropagationResult;
import com.taxonomy.dto.RelationOrigin;
import com.taxonomy.dto.RequirementRelationshipView;
import com.taxonomy.dto.TaxonomyRelationDto;
import com.taxonomy.model.SeedType;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* Builds the list of {@link RequirementRelationshipView} instances from the
* relevance-propagation traversal result.
*
* <p>Also contains the utility methods {@link #isSeedOriginRelation} and
* {@link #parseSeedType}, which classify and tag each relation.
*
* <p>This step is a pure function with no Spring or repository dependencies.
*
* <p><b>Core invariant</b> — this step must run after element-build and before
* provisional-relation. Do not disable it.
*/
@Service
public class RelationshipBuildStep implements ArchitecturePipelineStep {
/** Stable pipeline step ID. */
public static final String STEP_ID = "relationship-build";
@Override
public String id() { return STEP_ID; }
@Override
public int order() { return 500; }
@Override
public ArchitecturePipelineStepDescriptor descriptor() {
return new ArchitecturePipelineStepDescriptor(id(), order(), enabledByDefault(), true);
}
/**
* Builds relationships from the propagation result and stores them in the context.
*/
@Override
public void apply(ArchitectureViewContext ctx) {
ctx.setRelationships(build(ctx.getPropagation()));
}
/**
* Builds the relationship list from a propagation result.
* Package-private for unit testing.
*/
List<RequirementRelationshipView> build(PropagationResult propagation) {
List<RequirementRelationshipView> relationships = new ArrayList<>();
Set<String> includedNodeCodes = propagation.getRelevanceMap().keySet();
for (PropagationResult.TraversedRelation tr : propagation.getTraversedRelations()) {
TaxonomyRelationDto rel = tr.getRelation();
if (!includedNodeCodes.contains(rel.getSourceCode()) ||
!includedNodeCodes.contains(rel.getTargetCode())) {
continue;
}
RequirementRelationshipView rv = new RequirementRelationshipView();
rv.setRelationId(rel.getId());
rv.setSourceCode(rel.getSourceCode());
rv.setTargetCode(rel.getTargetCode());
rv.setRelationType(rel.getRelationType());
rv.setPropagatedRelevance(tr.getPropagatedRelevance());
rv.setHopDistance(tr.getHopDistance());
rv.setIncludedBecause(tr.getReason());
rv.setConfidence(tr.getPropagatedRelevance());
boolean isSeedRelation = isSeedOriginRelation(rel);
if (isSeedRelation) {
rv.setOrigin(RelationOrigin.TAXONOMY_SEED);
rv.setSeedType(parseSeedType(rel.getProvenance()));
rv.setDerivationReason("Seed relation: " + rel.getSourceCode()
+ " → " + rel.getTargetCode());
} else {
rv.setOrigin(RelationOrigin.PROPAGATED_TRACE);
rv.setDerivationReason("BFS propagation hop " + tr.getHopDistance());
}
relationships.add(rv);
}
// Deduplicate by relationId, keeping the entry with highest propagatedRelevance
Map<Long, RequirementRelationshipView> deduped = new LinkedHashMap<>();
for (RequirementRelationshipView rv : relationships) {
Long key = rv.getRelationId();
RequirementRelationshipView existing = deduped.get(key);
if (existing == null || rv.getPropagatedRelevance() > existing.getPropagatedRelevance()) {
deduped.put(key, rv);
}
}
return new ArrayList<>(deduped.values());
}
/**
* Returns {@code true} if the underlying relation is a seed-origin relation.
*
* <p>Seed relations are characterised by both endpoints being root taxonomy codes
* (two-letter codes without a hyphen, e.g. CP → CR).
*/
public static boolean isSeedOriginRelation(TaxonomyRelationDto rel) {
String src = rel.getSourceCode();
String tgt = rel.getTargetCode();
return src != null && tgt != null
&& !src.contains("-") && !tgt.contains("-");
}
/**
* Parses a {@link SeedType} from the relation provenance string.
* Falls back to {@link SeedType#TYPE_DEFAULT} when no specific type is recorded.
*/
public static SeedType parseSeedType(String provenance) {
if (provenance == null) return SeedType.TYPE_DEFAULT;
String upper = provenance.toUpperCase(Locale.ROOT);
if (upper.contains("FRAMEWORK")) return SeedType.FRAMEWORK_SEED;
if (upper.contains("SOURCE_DERIVED") || upper.contains("DERIVED")) return SeedType.SOURCE_DERIVED;
return SeedType.TYPE_DEFAULT;
}
}