ImpactEndpointSelector.java

package com.taxonomy.architecture.service;

import com.taxonomy.dto.RequirementElementView;

import java.util.*;

/**
 * Selects all qualified impact endpoints from a list of leaf nodes in the same
 * taxonomy category, rather than picking only the single "best" leaf.
 *
 * <p>A leaf qualifies as an impact endpoint when its composite score
 * (relevance × 0.5 + normalised depth × 0.3 + impact-selection bonus × 0.2)
 * exceeds the qualification threshold, or when it is an anchor, or when it was
 * explicitly marked as selected-for-impact by the {@link ArchitectureImpactSelector}.
 */
class ImpactEndpointSelector {

    /** Minimum composite score for automatic qualification. */
    static final double QUALIFICATION_THRESHOLD = 0.35;

    /** Maximum depth used for normalisation. */
    private static final double MAX_DEPTH = 5.0;

    /**
     * Returns all qualified impact endpoints from the provided leaves.
     * The result is ordered by composite score descending.
     *
     * @param leaves non-empty list of leaf elements from the same taxonomy category
     * @return list of qualified endpoints (never empty — at least the best leaf is always included)
     */
    List<RequirementElementView> selectEndpoints(List<RequirementElementView> leaves) {
        if (leaves == null || leaves.isEmpty()) {
            return List.of();
        }

        record Scored(RequirementElementView element, double score) {}

        List<Scored> scored = new ArrayList<>();
        for (RequirementElementView leaf : leaves) {
            double relevanceComponent = leaf.getRelevance() * 0.5;
            double depthComponent = Math.min(pathDepth(leaf.getHierarchyPath()) / MAX_DEPTH, 1.0) * 0.3;
            double impactBonus = leaf.isSelectedForImpact() ? 0.2 : 0.0;
            double compositeScore = relevanceComponent + depthComponent + impactBonus;
            scored.add(new Scored(leaf, compositeScore));
        }

        scored.sort(Comparator.comparingDouble(Scored::score).reversed());

        List<RequirementElementView> qualified = new ArrayList<>();
        for (Scored s : scored) {
            if (s.score() >= QUALIFICATION_THRESHOLD
                    || s.element().isAnchor()
                    || s.element().isSelectedForImpact()) {
                qualified.add(s.element());
            }
        }

        // Guarantee at least the best leaf is always included
        if (qualified.isEmpty()) {
            qualified.add(scored.get(0).element());
        }

        return qualified;
    }

    /**
     * Returns the depth of a hierarchy path by counting path segments.
     */
    private static int pathDepth(String path) {
        if (path == null || path.isEmpty()) return 0;
        int count = 1;
        int idx = 0;
        while ((idx = path.indexOf(" > ", idx)) >= 0) {
            count++;
            idx += 3;
        }
        return count;
    }
}