ArchitecturePipelineInvariantValidator.java

package com.taxonomy.architecture.pipeline;

import com.taxonomy.dto.RequirementAnchor;
import com.taxonomy.dto.RequirementElementView;
import com.taxonomy.dto.RequirementRelationshipView;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Validates cross-step invariants after each architecture pipeline stage and
 * before the final view is returned. A faulty custom step therefore fails at
 * the point where it corrupts the shared context instead of producing a
 * plausible but inconsistent architecture result.
 */
@Component
public class ArchitecturePipelineInvariantValidator {

    public void afterStep(ArchitecturePipelineStep step, ArchitectureViewContext context) {
        if (step == null) {
            throw new IllegalStateException("Architecture pipeline executed a null step");
        }
        validateCollections(context, step.id());
        validateAnchors(context.getAnchors(), step.id());
        validateElements(context.getElements(), step.id());
        validateRelationships(context.getRelationships(), step.id());

        if (step.order() >= 700 && context.getMaxArchitectureNodes() > 0
                && context.getElements().size() > context.getMaxArchitectureNodes()) {
            throw new IllegalStateException(
                    "Pipeline invariant failed after '%s': %d elements exceed maxArchitectureNodes=%d"
                            .formatted(step.id(), context.getElements().size(),
                                    context.getMaxArchitectureNodes()));
        }
    }

    public void beforeReturn(ArchitectureViewContext context) {
        validateCollections(context, "finalization");
        validateAnchors(context.getAnchors(), "finalization");
        validateElements(context.getElements(), "finalization");
        validateRelationships(context.getRelationships(), "finalization");

        Set<String> elementCodes = new HashSet<>();
        for (RequirementElementView element : context.getElements()) {
            elementCodes.add(element.getNodeCode());
        }
        for (RequirementRelationshipView relationship : context.getRelationships()) {
            if (!elementCodes.contains(relationship.getSourceCode())
                    || !elementCodes.contains(relationship.getTargetCode())) {
                throw new IllegalStateException(
                        "Pipeline invariant failed during finalization: relation %s -> %s references an element outside the view"
                                .formatted(relationship.getSourceCode(), relationship.getTargetCode()));
            }
        }
        for (RequirementAnchor anchor : context.getAnchors()) {
            if (!elementCodes.contains(anchor.getNodeCode())) {
                throw new IllegalStateException(
                        "Pipeline invariant failed during finalization: anchor %s is absent from included elements"
                                .formatted(anchor.getNodeCode()));
            }
        }
    }

    private void validateCollections(ArchitectureViewContext context, String phase) {
        if (context == null) {
            throw new IllegalStateException("Pipeline invariant failed after '" + phase + "': context is null");
        }
        if (context.getAnchors() == null || context.getElements() == null
                || context.getRelationships() == null) {
            throw new IllegalStateException(
                    "Pipeline invariant failed after '%s': anchors, elements and relationships must not be null"
                            .formatted(phase));
        }
    }

    private void validateAnchors(List<RequirementAnchor> anchors, String phase) {
        Set<String> seen = new HashSet<>();
        for (RequirementAnchor anchor : anchors) {
            if (anchor == null || isBlank(anchor.getNodeCode())) {
                throw new IllegalStateException(
                        "Pipeline invariant failed after '%s': anchor code must not be blank"
                                .formatted(phase));
            }
            if (!seen.add(anchor.getNodeCode())) {
                throw new IllegalStateException(
                        "Pipeline invariant failed after '%s': duplicate anchor %s"
                                .formatted(phase, anchor.getNodeCode()));
            }
        }
    }

    private void validateElements(List<RequirementElementView> elements, String phase) {
        Set<String> seen = new HashSet<>();
        for (RequirementElementView element : elements) {
            if (element == null || isBlank(element.getNodeCode())) {
                throw new IllegalStateException(
                        "Pipeline invariant failed after '%s': element code must not be blank"
                                .formatted(phase));
            }
            if (!seen.add(element.getNodeCode())) {
                throw new IllegalStateException(
                        "Pipeline invariant failed after '%s': duplicate element %s"
                                .formatted(phase, element.getNodeCode()));
            }
            if (!Double.isFinite(element.getRelevance()) || element.getRelevance() < 0) {
                throw new IllegalStateException(
                        "Pipeline invariant failed after '%s': invalid relevance for %s"
                                .formatted(phase, element.getNodeCode()));
            }
            if (element.getHopDistance() < 0 || element.getDirectLlmScore() < 0
                    || element.getDirectLlmScore() > 100) {
                throw new IllegalStateException(
                        "Pipeline invariant failed after '%s': invalid score/hop values for %s"
                                .formatted(phase, element.getNodeCode()));
            }
        }
    }

    private void validateRelationships(List<RequirementRelationshipView> relationships, String phase) {
        for (RequirementRelationshipView relationship : relationships) {
            if (relationship == null || isBlank(relationship.getSourceCode())
                    || isBlank(relationship.getTargetCode())
                    || isBlank(relationship.getRelationType())) {
                throw new IllegalStateException(
                        "Pipeline invariant failed after '%s': relation endpoints and type must not be blank"
                                .formatted(phase));
            }
            if (!Double.isFinite(relationship.getPropagatedRelevance())
                    || relationship.getPropagatedRelevance() < 0
                    || relationship.getHopDistance() < 0
                    || relationship.getConfidence() < 0
                    || relationship.getConfidence() > 1) {
                throw new IllegalStateException(
                        "Pipeline invariant failed after '%s': invalid values for relation %s -> %s"
                                .formatted(phase, relationship.getSourceCode(), relationship.getTargetCode()));
            }
        }
    }

    private boolean isBlank(String value) {
        return value == null || value.isBlank();
    }
}