ArchitectureViewPipeline.java
package com.taxonomy.architecture.pipeline;
import com.taxonomy.dto.RequirementArchitectureView;
import com.taxonomy.dto.RequirementElementView;
import com.taxonomy.dto.RequirementRelationshipView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* Executes the ordered architecture-view steps and validates their shared
* context after every stage. Extension steps may enrich the pipeline but cannot
* silently violate core element, relation, anchor, score, or node-limit invariants.
*/
@Service
public class ArchitectureViewPipeline {
private static final Logger log = LoggerFactory.getLogger(ArchitectureViewPipeline.class);
private final ArchitecturePipelineStepRegistry registry;
private final ArchitecturePipelineInvariantValidator invariantValidator;
public ArchitectureViewPipeline(ArchitecturePipelineStepRegistry registry,
ArchitecturePipelineInvariantValidator invariantValidator) {
this.registry = registry;
this.invariantValidator = invariantValidator;
}
public RequirementArchitectureView execute(ArchitectureViewContext context) {
RequirementArchitectureView view = context.getView();
List<ArchitecturePipelineStep> enabledSteps = registry.getEnabledSteps();
ArchitecturePipelineStep firstEnabledStep = enabledSteps.stream()
.findFirst()
.orElseThrow(() -> new IllegalStateException(
"Architecture pipeline requires at least one enabled step, specifically %s as the first step"
.formatted(AnchorSelectionStep.STEP_ID)));
if (!AnchorSelectionStep.STEP_ID.equals(firstEnabledStep.id())) {
throw new IllegalStateException(
"Architecture pipeline must execute %s first, but found %s"
.formatted(AnchorSelectionStep.STEP_ID, firstEnabledStep.id()));
}
firstEnabledStep.apply(context);
invariantValidator.afterStep(firstEnabledStep, context);
view.setAnchors(context.getAnchors());
if (context.getAnchors().isEmpty()) {
view.getNotes().add("No nodes met the anchor threshold; architecture view is empty.");
invariantValidator.beforeReturn(context);
return view;
}
for (ArchitecturePipelineStep step : enabledSteps.subList(1, enabledSteps.size())) {
step.apply(context);
invariantValidator.afterStep(step, context);
}
populatePresenceReasons(context.getElements(), context.getRelationships());
populateParentNodeCodes(context.getElements());
invariantValidator.beforeReturn(context);
view.setIncludedElements(context.getElements());
view.setIncludedRelationships(context.getRelationships());
if (context.isUsedProvisional()) {
view.getNotes().add("Architecture view built using AI-suggested provisional relations "
+ "(not yet confirmed).");
} else if (context.getRelationships().isEmpty()
&& context.getElements().size() == context.getAnchors().size()) {
view.getNotes().add("No traversable relations found for anchor nodes; "
+ "only direct matches are included.");
}
int maxHop = context.getElements().stream()
.mapToInt(RequirementElementView::getHopDistance).max().orElse(0);
view.setTotalAnchors(context.getAnchors().size());
view.setTotalElements(context.getElements().size());
view.setTotalRelationships(context.getRelationships().size());
view.setMaxHopDistance(maxHop);
log.info("RequirementArchitectureView summary: anchors={}, elements={}, relationships={}, maxHopDistance={}",
context.getAnchors().size(), context.getElements().size(),
context.getRelationships().size(), maxHop);
logPropagationSummary(context);
return view;
}
private static void logPropagationSummary(ArchitectureViewContext context) {
for (var anchor : context.getAnchors()) {
List<RequirementElementView> propagated = context.getElements().stream()
.filter(element -> !element.isAnchor()
&& element.getIncludedBecause() != null
&& element.getIncludedBecause().contains(anchor.getNodeCode()))
.toList();
if (propagated.isEmpty()) continue;
StringBuilder summary = new StringBuilder("anchor ").append(anchor.getNodeCode());
for (RequirementElementView element : propagated) {
summary.append("\n → ").append(element.getNodeCode())
.append(" (")
.append(String.format(Locale.US, "%.2f", element.getRelevance()))
.append(')');
}
log.debug(summary.toString());
}
}
private static void populatePresenceReasons(List<RequirementElementView> elements,
List<RequirementRelationshipView> relationships) {
for (RequirementElementView element : elements) {
if (element.getPresenceReason() != null) continue;
StringBuilder reason = new StringBuilder();
reason.append(element.getNodeCode());
if (element.getTitle() != null) reason.append(" (").append(element.getTitle()).append(')');
reason.append(": ");
if (element.getOrigin() != null) {
reason.append(element.getOrigin().name().toLowerCase().replace('_', ' '));
} else {
reason.append("included via propagation");
}
if (element.getDirectLlmScore() > 0) {
reason.append(", LLM score ").append(element.getDirectLlmScore());
}
if (element.getHopDistance() > 0) {
reason.append(", ").append(element.getHopDistance()).append(" hop(s)");
}
element.setPresenceReason(reason.toString());
}
for (RequirementRelationshipView relationship : relationships) {
if (relationship.getPresenceReason() != null) continue;
StringBuilder reason = new StringBuilder();
reason.append(relationship.getSourceCode())
.append(" → ").append(relationship.getTargetCode())
.append(": ").append(relationship.getRelationCategory());
if (relationship.getOrigin() != null) {
reason.append(", ")
.append(relationship.getOrigin().name().toLowerCase().replace('_', ' '));
}
if (relationship.getConfidence() > 0) {
reason.append(String.format(", confidence %.0f%%", relationship.getConfidence() * 100));
}
relationship.setPresenceReason(reason.toString());
}
}
private static void populateParentNodeCodes(List<RequirementElementView> elements) {
Set<String> codes = new HashSet<>();
for (RequirementElementView element : elements) {
codes.add(element.getNodeCode());
}
for (RequirementElementView element : elements) {
String hierarchyPath = element.getHierarchyPath();
if (hierarchyPath == null || hierarchyPath.isEmpty()) continue;
String[] parts = hierarchyPath.split("\\s*>\\s*");
for (int index = parts.length - 2; index >= 0; index--) {
String candidate = parts[index].trim();
if (codes.contains(candidate)) {
element.setParentNodeCode(candidate);
break;
}
}
}
}
}