ElementBuildStep.java
package com.taxonomy.architecture.pipeline;
import com.taxonomy.catalog.model.TaxonomyNode;
import com.taxonomy.catalog.repository.TaxonomyNodeRepository;
import com.taxonomy.catalog.service.PropagationResult;
import com.taxonomy.catalog.service.TaxonomyService;
import com.taxonomy.dto.NodeOrigin;
import com.taxonomy.dto.RequirementElementView;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Builds the initial list of {@link RequirementElementView} instances from
* the relevance propagation result.
*
* <p>Each node in the propagation result is looked up in the taxonomy to
* populate its title, taxonomy sheet, depth, and hierarchy path. Elements
* are sorted anchors-first, then by relevance descending.
*
* <p><b>Core invariant</b> — this step must run after relevance-propagation and
* before leaf-enrichment. Do not disable it.
*/
@Service
public class ElementBuildStep implements ArchitecturePipelineStep {
/** Stable pipeline step ID. */
public static final String STEP_ID = "element-build";
private final TaxonomyNodeRepository nodeRepository;
private final TaxonomyService taxonomyService;
public ElementBuildStep(TaxonomyNodeRepository nodeRepository,
TaxonomyService taxonomyService) {
this.nodeRepository = nodeRepository;
this.taxonomyService = taxonomyService;
}
@Override
public String id() { return STEP_ID; }
@Override
public int order() { return 300; }
@Override
public ArchitecturePipelineStepDescriptor descriptor() {
return new ArchitecturePipelineStepDescriptor(id(), order(), enabledByDefault(), true);
}
@Override
public void apply(ArchitectureViewContext ctx) {
PropagationResult propagation = ctx.getPropagation();
List<RequirementElementView> elements = new ArrayList<>();
for (Map.Entry<String, Double> entry : propagation.getRelevanceMap().entrySet()) {
String nodeCode = entry.getKey();
double relevance = entry.getValue();
int hopDistance = propagation.getHopDistanceMap().getOrDefault(nodeCode, 0);
String reason = propagation.getReasonMap().getOrDefault(nodeCode, "unknown");
RequirementElementView element = new RequirementElementView();
element.setNodeCode(nodeCode);
element.setRelevance(relevance);
element.setHopDistance(hopDistance);
element.setAnchor(hopDistance == 0);
element.setIncludedBecause(reason);
element.setOrigin(hopDistance == 0 ? NodeOrigin.DIRECT_SCORED : NodeOrigin.PROPAGATED);
// Refine origin: non-anchor root codes (no '-') reached through propagation
// are seed-context nodes rather than independently propagated results.
if (hopDistance > 0 && !nodeCode.contains("-")) {
element.setOrigin(NodeOrigin.SEED_CONTEXT);
}
Optional<TaxonomyNode> nodeOpt = nodeRepository.findByCode(nodeCode);
if (nodeOpt.isPresent()) {
TaxonomyNode node = nodeOpt.get();
element.setTitle(node.getNameEn());
element.setTaxonomySheet(node.getTaxonomyRoot());
element.setTaxonomyDepth(node.getLevel());
}
element.setDirectLlmScore((int) Math.round(relevance * 100));
element.setHierarchyPath(ctx.buildHierarchyPath(nodeCode, taxonomyService));
elements.add(element);
}
// Sort: anchors first, then by relevance descending
elements.sort(Comparator
.comparing(RequirementElementView::isAnchor).reversed()
.thenComparing(Comparator.comparingDouble(RequirementElementView::getRelevance).reversed()));
ctx.setElements(elements);
}
}