RelationValidationService.java

package com.taxonomy.relations.service;

import com.taxonomy.catalog.model.TaxonomyNode;
import com.taxonomy.dto.TaxonomyNodeDto;
import com.taxonomy.model.RelationType;
import com.taxonomy.workspace.service.RepositoryContext;
import org.springframework.stereotype.Service;

/**
 * Validates a candidate relation and computes a confidence score.
 *
 * <p>Repository/workspace/branch duplicate detection is performed by
 * {@link RelationProposalService} against one immutable identity snapshot.
 * Review-history feedback is delegated with the same explicit context.</p>
 */
@Service
public class RelationValidationService {

    private final RelationCompatibilityMatrix compatibilityMatrix;
    private final RelationQualityService qualityService;

    public RelationValidationService(
            RelationCompatibilityMatrix compatibilityMatrix,
            RelationQualityService qualityService) {
        this.compatibilityMatrix = compatibilityMatrix;
        this.qualityService = qualityService;
    }

    /** Validates and scores a candidate in the proposal-generation context. */
    public ValidationResult validate(
            TaxonomyNode source,
            TaxonomyNodeDto candidateTarget,
            RelationType relationType,
            int rank,
            int totalCandidates,
            RepositoryContext context) {
        if (source.getCode().equals(candidateTarget.getCode())) {
            return ValidationResult.fail("Self-relation not allowed");
        }

        if (!compatibilityMatrix.isCompatible(
                source.getTaxonomyRoot(),
                candidateTarget.getTaxonomyRoot(),
                relationType)) {
            return ValidationResult.fail(
                    "Incompatible roots: " + source.getTaxonomyRoot()
                    + " to " + candidateTarget.getTaxonomyRoot()
                    + " for " + relationType);
        }

        double rankConfidence = computeConfidence(rank, totalCandidates);
        double historyWeight = qualityService.acceptanceHistoryWeight(
                source.getTaxonomyRoot(),
                candidateTarget.getTaxonomyRoot(),
                relationType,
                context);
        double confidence = 0.80 * rankConfidence + 0.20 * historyWeight;

        String rationale = String.format(
                "%s [%s] to %s [%s] (%s), rank %d/%d",
                source.getCode(), source.getTaxonomyRoot(),
                candidateTarget.getCode(), candidateTarget.getTaxonomyRoot(),
                relationType, rank + 1, totalCandidates);

        return ValidationResult.pass(confidence, rationale);
    }

    /**
     * Computes confidence from the rank position.
     * Rank 0 has the highest confidence, later ranks decline to 0.3.
     */
    public double computeConfidence(int rank, int totalCandidates) {
        if (totalCandidates <= 1) {
            return 0.9;
        }
        double ratio = (double) rank / (totalCandidates - 1);
        return 0.95 - (0.65 * ratio);
    }

    public static class ValidationResult {
        private final boolean valid;
        private final double confidence;
        private final String rationale;

        private ValidationResult(
                boolean valid,
                double confidence,
                String rationale) {
            this.valid = valid;
            this.confidence = confidence;
            this.rationale = rationale;
        }

        public static ValidationResult pass(
                double confidence,
                String rationale) {
            return new ValidationResult(true, confidence, rationale);
        }

        public static ValidationResult fail(String reason) {
            return new ValidationResult(false, 0.0, reason);
        }

        public boolean isValid() {
            return valid;
        }

        public double getConfidence() {
            return confidence;
        }

        public String getRationale() {
            return rationale;
        }
    }
}