ScoringTraceSelector.java
package com.taxonomy.architecture.service;
import com.taxonomy.catalog.model.TaxonomyNode;
import com.taxonomy.catalog.service.TaxonomyService;
import com.taxonomy.dto.NodeOrigin;
import com.taxonomy.dto.RequirementAnchor;
import com.taxonomy.dto.RequirementElementView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* Selects all nodes that form the hierarchical scoring path for each anchor,
* so the UI can present a transparent trace from root through intermediates to
* the directly-scored leaf.
*
* <p>For every anchor the full path (root → intermediate → anchor) is
* reconstructed using {@link TaxonomyService#getPathToRoot(String)}. Each
* intermediate node is marked as {@link NodeOrigin#TRACE_INTERMEDIATE} and
* every directly-scored node is marked as {@link NodeOrigin#DIRECT_SCORED}.
*/
@Service
public class ScoringTraceSelector {
private static final Logger log = LoggerFactory.getLogger(ScoringTraceSelector.class);
private final TaxonomyService taxonomyService;
public ScoringTraceSelector(TaxonomyService taxonomyService) {
this.taxonomyService = taxonomyService;
}
/**
* Builds scoring trace entries for every anchor node.
*
* @param allScores map of nodeCode → integer score (0–100)
* @param anchors the anchor nodes selected for the architecture view
* @return list of trace entries with fully-populated origin and scoring path
*/
public List<RequirementElementView> buildTrace(Map<String, Integer> allScores,
List<RequirementAnchor> anchors) {
Map<String, RequirementElementView> traceMap = new LinkedHashMap<>();
for (RequirementAnchor anchor : anchors) {
String anchorCode = anchor.getNodeCode();
List<TaxonomyNode> pathToRoot = taxonomyService.getPathToRoot(anchorCode);
if (pathToRoot.isEmpty()) {
// No path found — add anchor node directly
addOrUpdate(traceMap, anchorCode, allScores, NodeOrigin.DIRECT_SCORED, null);
continue;
}
// pathToRoot is ordered root → leaf; build the scoring path string
StringBuilder scoringPath = new StringBuilder();
for (int i = 0; i < pathToRoot.size(); i++) {
TaxonomyNode pathNode = pathToRoot.get(i);
String code = pathNode.getCode();
int score = allScores.getOrDefault(code, 0);
if (i > 0) scoringPath.append(" > ");
scoringPath.append(code).append("(").append(score).append("%)");
boolean isAnchorNode = code.equals(anchorCode);
NodeOrigin origin = isAnchorNode ? NodeOrigin.DIRECT_SCORED
: NodeOrigin.TRACE_INTERMEDIATE;
RequirementElementView entry = addOrUpdate(traceMap, code, allScores, origin, pathNode);
entry.setScoringPath(scoringPath.toString());
entry.setTaxonomyDepth(pathNode.getLevel());
}
}
List<RequirementElementView> result = new ArrayList<>(traceMap.values());
log.debug("Built scoring trace with {} entries for {} anchors", result.size(), anchors.size());
return result;
}
private RequirementElementView addOrUpdate(Map<String, RequirementElementView> traceMap,
String code, Map<String, Integer> allScores,
NodeOrigin origin, TaxonomyNode node) {
RequirementElementView existing = traceMap.get(code);
if (existing != null) {
// Upgrade origin: DIRECT_SCORED takes priority over TRACE_INTERMEDIATE
if (origin == NodeOrigin.DIRECT_SCORED
&& existing.getOrigin() == NodeOrigin.TRACE_INTERMEDIATE) {
existing.setOrigin(NodeOrigin.DIRECT_SCORED);
}
return existing;
}
int score = allScores.getOrDefault(code, 0);
RequirementElementView entry = new RequirementElementView();
entry.setNodeCode(code);
entry.setOrigin(origin);
entry.setDirectLlmScore(score);
entry.setRelevance(score / 100.0);
entry.setAnchor(origin == NodeOrigin.DIRECT_SCORED);
if (node != null) {
entry.setTitle(node.getNameEn());
entry.setTaxonomySheet(node.getTaxonomyRoot());
entry.setTaxonomyDepth(node.getLevel());
}
traceMap.put(code, entry);
return entry;
}
}