ProvisionalRelationStep.java
package com.taxonomy.architecture.pipeline;
import com.taxonomy.catalog.model.TaxonomyNode;
import com.taxonomy.catalog.repository.TaxonomyNodeRepository;
import com.taxonomy.catalog.service.TaxonomyService;
import com.taxonomy.dto.NodeOrigin;
import com.taxonomy.dto.RelationHypothesisDto;
import com.taxonomy.dto.RequirementElementView;
import com.taxonomy.dto.RequirementRelationshipView;
import com.taxonomy.dto.RelationOrigin;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Injects provisional (AI-suggested) relation hypotheses as virtual edges
* when no confirmed relations exist.
*
* <p>When the relationship list is empty but provisional relations are available,
* this step adds the hypothesis endpoints to the element list (if not already present)
* and creates corresponding {@link RequirementRelationshipView} entries marked as
* {@code provisional (AI-suggested, not yet confirmed)}.
*
* <p>This ensures the architecture view is immediately useful even before any
* confirmed taxonomy relations have been saved.
*
* <p><b>Safe extension point</b> — this step may be replaced or augmented
* to change how provisional relations are injected, provided the context
* element and relationship lists remain in a consistent state on exit.
*/
@Service
public class ProvisionalRelationStep implements ArchitecturePipelineStep {
/** Stable pipeline step ID. */
public static final String STEP_ID = "provisional-relation";
private final TaxonomyNodeRepository nodeRepository;
private final TaxonomyService taxonomyService;
public ProvisionalRelationStep(TaxonomyNodeRepository nodeRepository,
TaxonomyService taxonomyService) {
this.nodeRepository = nodeRepository;
this.taxonomyService = taxonomyService;
}
@Override
public String id() { return STEP_ID; }
@Override
public int order() { return 600; }
@Override
public void apply(ArchitectureViewContext ctx) {
List<RequirementRelationshipView> relationships = ctx.getRelationships();
List<RelationHypothesisDto> provisionalRelations = ctx.getProvisionalRelations();
if (!relationships.isEmpty()
|| provisionalRelations == null
|| provisionalRelations.isEmpty()) {
return;
}
List<RequirementElementView> elements = ctx.getElements();
Set<String> includedCodes = elements.stream()
.map(RequirementElementView::getNodeCode)
.collect(Collectors.toSet());
boolean addedProvisional = false;
for (RelationHypothesisDto hyp : provisionalRelations) {
if (!hasText(hyp.getSourceCode()) || !hasText(hyp.getTargetCode())) {
continue;
}
ensureElement(elements, includedCodes, hyp.getSourceCode(), hyp.getSourceName(),
ctx.getScores(), ctx);
ensureElement(elements, includedCodes, hyp.getTargetCode(), hyp.getTargetName(),
ctx.getScores(), ctx);
RequirementRelationshipView rv = new RequirementRelationshipView();
rv.setSourceCode(hyp.getSourceCode());
rv.setTargetCode(hyp.getTargetCode());
rv.setRelationType(hyp.getRelationType());
rv.setPropagatedRelevance(hyp.getConfidence());
rv.setHopDistance(0);
rv.setIncludedBecause("provisional (AI-suggested, not yet confirmed)");
relationships.add(rv);
addedProvisional = true;
}
ctx.setUsedProvisional(addedProvisional);
}
private void ensureElement(List<RequirementElementView> elements,
Set<String> includedCodes,
String nodeCode, String nodeName,
java.util.Map<String, Integer> scores,
ArchitectureViewContext ctx) {
if (includedCodes.contains(nodeCode)) {
return;
}
RequirementElementView element = new RequirementElementView();
element.setNodeCode(nodeCode);
element.setRelevance(scores.getOrDefault(nodeCode, 0) / 100.0);
element.setHopDistance(0);
element.setAnchor(false);
element.setIncludedBecause("provisional relation endpoint");
element.setOrigin(NodeOrigin.SEED_CONTEXT);
element.setDirectLlmScore(scores.getOrDefault(nodeCode, 0));
element.setTitle(nodeName);
Optional<TaxonomyNode> nodeOpt = nodeRepository.findByCode(nodeCode);
if (nodeOpt.isPresent()) {
TaxonomyNode node = nodeOpt.get();
if (element.getTitle() == null) {
element.setTitle(node.getNameEn());
}
element.setTaxonomySheet(node.getTaxonomyRoot());
}
element.setHierarchyPath(ctx.buildHierarchyPath(nodeCode, taxonomyService));
elements.add(element);
includedCodes.add(nodeCode);
}
private static boolean hasText(String value) {
return value != null && !value.isBlank();
}
}