RelationValidationService.java
package com.taxonomy.relations.service;
import com.taxonomy.dto.TaxonomyNodeDto;
import com.taxonomy.model.RelationType;
import com.taxonomy.catalog.model.TaxonomyNode;
import com.taxonomy.catalog.repository.TaxonomyRelationRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* Validates a candidate relation and computes a confidence score.
*
* <p>Validation rules:
* <ol>
* <li>Compatibility — source/target roots must be allowed for the relation type.</li>
* <li>Duplicate check — relation must not already exist.</li>
* <li>Self-relation check — source must differ from target.</li>
* </ol>
*
* <p>Confidence is derived from the candidate's search rank position
* (higher rank → higher confidence).
*/
@Service
public class RelationValidationService {
private static final Logger log = LoggerFactory.getLogger(RelationValidationService.class);
private final RelationCompatibilityMatrix compatibilityMatrix;
private final TaxonomyRelationRepository relationRepository;
private final RelationQualityService qualityService;
public RelationValidationService(RelationCompatibilityMatrix compatibilityMatrix,
TaxonomyRelationRepository relationRepository,
RelationQualityService qualityService) {
this.compatibilityMatrix = compatibilityMatrix;
this.relationRepository = relationRepository;
this.qualityService = qualityService;
}
/**
* Validates a candidate relation.
*
* @return a {@link ValidationResult} with pass/fail and confidence
*/
public ValidationResult validate(TaxonomyNode source,
TaxonomyNodeDto candidateTarget,
RelationType relationType,
int rank,
int totalCandidates) {
// Self-relation
if (source.getCode().equals(candidateTarget.getCode())) {
return ValidationResult.fail("Self-relation not allowed");
}
// Compatibility check
if (!compatibilityMatrix.isCompatible(
source.getTaxonomyRoot(),
candidateTarget.getTaxonomyRoot(),
relationType)) {
return ValidationResult.fail(
"Incompatible roots: " + source.getTaxonomyRoot()
+ " → " + candidateTarget.getTaxonomyRoot()
+ " for " + relationType);
}
// Duplicate check (already exists as a persisted relation)
boolean exists = !relationRepository
.findBySourceNodeCodeAndRelationTypeIn(
source.getCode(),
java.util.List.of(relationType))
.stream()
.filter(r -> r.getTargetNode().getCode().equals(candidateTarget.getCode()))
.toList()
.isEmpty();
if (exists) {
return ValidationResult.fail("Relation already exists");
}
// Confidence: 80% rank-based + 20% acceptance history feedback
double rankConfidence = computeConfidence(rank, totalCandidates);
double historyWeight = qualityService.acceptanceHistoryWeight(
source.getTaxonomyRoot(),
candidateTarget.getTaxonomyRoot(),
relationType);
double confidence = 0.80 * rankConfidence + 0.20 * historyWeight;
String rationale = String.format(
"%s [%s] → %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 → highest confidence (0.95), higher ranks → lower confidence (min 0.3).
*/
public double computeConfidence(int rank, int totalCandidates) {
if (totalCandidates <= 1) return 0.9;
// Linear decay from 0.95 to 0.3
double ratio = (double) rank / (totalCandidates - 1);
return 0.95 - (0.65 * ratio);
}
// ── Inner result record ───────────────────────────────────────────────────
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; }
}
}