DecisionRationaleScoreSemanticsAdapter.java
package com.taxonomy.architecture.decision;
import com.taxonomy.architecture.decision.DecisionRationaleReport.ChildDecision;
import com.taxonomy.architecture.decision.DecisionRationaleReport.DecisionChapter;
import com.taxonomy.architecture.decision.DecisionRationaleReport.ExecutiveSummary;
import com.taxonomy.architecture.decision.DecisionRationaleReport.LeafCandidate;
import com.taxonomy.architecture.decision.DecisionRationaleReport.PathStep;
import com.taxonomy.architecture.decision.DecisionRationaleReport.ReportMetadata;
import com.taxonomy.dto.AnalysisScoreDetail;
import com.taxonomy.dto.AnalysisScoreSemanticsFingerprint;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
/**
* Applies explicit product-score semantics to the format-neutral decision report.
*
* <p>The hierarchy builder receives comparable effective scores so ranking remains valid. This
* adapter then restores the exact independent product suitability in the secondary percentage
* column and in the human-readable evidence while retaining the effective score for ranking.</p>
*/
@Component
public class DecisionRationaleScoreSemanticsAdapter {
public Map<String, String> enrichReasons(
Map<String, String> reasons,
Map<String, AnalysisScoreDetail> scoreDetails,
Locale locale) {
Map<String, String> result = new LinkedHashMap<>();
if (reasons != null) {
result.putAll(reasons);
}
boolean german = locale != null
&& "de".equalsIgnoreCase(locale.getLanguage());
safeDetails(scoreDetails).forEach((code, detail) -> {
if (detail == null || !detail.isProductSuitability()) {
return;
}
String evidence = evidence(detail, german);
String original = result.get(code);
if (original == null || original.isBlank()) {
result.put(code, evidence);
} else if (!original.contains(evidence)) {
result.put(code, evidence + " " + original.strip());
}
});
return result;
}
public DecisionRationaleReport adapt(
DecisionRationaleReport report,
Map<String, AnalysisScoreDetail> scoreDetails,
Locale locale) {
if (report == null) {
return null;
}
Map<String, AnalysisScoreDetail> details = safeDetails(scoreDetails);
boolean german = locale != null
&& "de".equalsIgnoreCase(locale.getLanguage());
ExecutiveSummary summary = report.executiveSummary();
ExecutiveSummary adaptedSummary = summary == null ? null : new ExecutiveSummary(
adaptLeaf(summary.leadingLeaf(), details, german),
summary.path().stream()
.map(step -> adaptPathStep(step, details, german))
.toList(),
summary.conciseConclusion(),
appendMethodology(summary.methodologyNote(), details, german));
List<DecisionChapter> chapters = report.chapters().stream()
.map(chapter -> adaptChapter(chapter, details, german))
.toList();
List<LeafCandidate> leaves = report.leadingLeaves().stream()
.map(leaf -> adaptLeaf(leaf, details, german))
.toList();
ReportMetadata metadata = report.metadata();
boolean alreadyAdapted = !details.isEmpty() && details.equals(report.scoreDetails());
if (metadata != null && !alreadyAdapted && !details.isEmpty()) {
metadata = metadata.withAnalysisSnapshotFingerprintSha256(
AnalysisScoreSemanticsFingerprint.extend(
metadata.analysisSnapshotFingerprintSha256(), details));
}
return new DecisionRationaleReport(
report.title(),
report.languageTag(),
report.requirement(),
report.status(),
metadata,
adaptedSummary,
chapters,
leaves,
report.warnings(),
report.productCoverageGaps(),
report.discrepancies(),
report.viewContext(),
details);
}
private DecisionChapter adaptChapter(
DecisionChapter chapter,
Map<String, AnalysisScoreDetail> details,
boolean german) {
List<ChildDecision> children = chapter.children().stream()
.map(child -> adaptChild(child, details, german))
.toList();
return new DecisionChapter(
chapter.number(),
chapter.parentCode(),
chapter.parentTitle(),
chapter.parentDescription(),
chapter.parentScore(),
chapter.hierarchyLevel(),
chapter.complete(),
chapter.decisionSummary(),
chapter.comparativeRationale(),
children,
chapter.missingChildCodes());
}
private ChildDecision adaptChild(
ChildDecision child,
Map<String, AnalysisScoreDetail> details,
boolean german) {
AnalysisScoreDetail detail = details.get(child.code());
if (detail == null || !detail.isProductSuitability()) {
return new ChildDecision(
child.code(), child.title(), child.description(), child.absoluteScore(),
bounded(child.localSharePercent()), child.rank(), child.leadingSibling(),
child.disposition(), child.reason(), child.reasonSource(), child.leaf());
}
return new ChildDecision(
child.code(), child.title(), child.description(), detail.effectiveRelevance(),
(double) detail.rawScore(), child.rank(), child.leadingSibling(),
child.disposition(), enrichedReason(child.reason(), detail, german),
child.reasonSource(), child.leaf());
}
private PathStep adaptPathStep(
PathStep step,
Map<String, AnalysisScoreDetail> details,
boolean german) {
AnalysisScoreDetail detail = details.get(step.code());
if (detail == null || !detail.isProductSuitability()) {
return new PathStep(
step.position(), step.code(), step.title(), step.absoluteScore(),
bounded(step.localSharePercent()), step.reason(), step.reasonSource());
}
return new PathStep(
step.position(), step.code(), step.title(), detail.effectiveRelevance(),
(double) detail.rawScore(), enrichedReason(step.reason(), detail, german),
step.reasonSource());
}
private LeafCandidate adaptLeaf(
LeafCandidate leaf,
Map<String, AnalysisScoreDetail> details,
boolean german) {
if (leaf == null) {
return null;
}
AnalysisScoreDetail detail = details.get(leaf.code());
if (detail == null || !detail.isProductSuitability()) {
return leaf;
}
return new LeafCandidate(
leaf.code(), leaf.title(), detail.effectiveRelevance(), leaf.taxonomyRoot(),
leaf.depth(), leaf.hierarchyPath(), enrichedReason(leaf.reason(), detail, german),
leaf.reasonSource());
}
private String appendMethodology(
String existing,
Map<String, AnalysisScoreDetail> details,
boolean german) {
boolean hasProducts = details.values().stream()
.anyMatch(detail -> detail != null && detail.isProductSuitability());
if (!hasProducts) {
return existing;
}
String addition = german
? "Konkrete Produkte werden mit einer unabhängigen Eignung von 0 bis 100 bewertet; für Rangfolge und Architektur wird die mit der Familienrelevanz gewichtete effektive Relevanz verwendet."
: "Concrete products receive an independent 0–100 suitability score; ranking and architecture use the effective relevance weighted by the product-family relevance.";
if (existing == null || existing.isBlank()) {
return addition;
}
return existing.contains(addition) ? existing : existing.strip() + " " + addition;
}
private String enrichedReason(
String original,
AnalysisScoreDetail detail,
boolean german) {
String evidence = evidence(detail, german);
if (original == null || original.isBlank()) {
return evidence;
}
return original.contains(evidence) ? original : evidence + " " + original.strip();
}
private String evidence(AnalysisScoreDetail detail, boolean german) {
return german
? "Produkteignung: " + detail.rawScore()
+ " %; effektive Relevanz: " + detail.effectiveRelevance() + "/100."
: "Product suitability: " + detail.rawScore()
+ "%; effective relevance: " + detail.effectiveRelevance() + "/100.";
}
private Double bounded(Double value) {
if (value == null || !Double.isFinite(value)) {
return value == null ? null : 0.0;
}
return Math.max(0.0, Math.min(100.0, value));
}
private Map<String, AnalysisScoreDetail> safeDetails(
Map<String, AnalysisScoreDetail> scoreDetails) {
if (scoreDetails == null || scoreDetails.isEmpty()) {
return Map.of();
}
Map<String, AnalysisScoreDetail> canonicalDetails = new TreeMap<>();
for (Map.Entry<String, AnalysisScoreDetail> entry : scoreDetails.entrySet()) {
if (entry.getKey() == null || entry.getKey().isBlank()
|| entry.getValue() == null) {
continue;
}
String code = entry.getKey().strip();
AnalysisScoreDetail detail = entry.getValue();
if (!code.equals(detail.nodeCode())) {
throw new IllegalArgumentException(
"Score detail key " + code
+ " does not match detail node code " + detail.nodeCode());
}
if (canonicalDetails.containsKey(code)) {
throw new IllegalArgumentException(
"Score details contain multiple entries for canonical node code " + code);
}
canonicalDetails.put(code, detail);
}
return Collections.unmodifiableMap(new LinkedHashMap<>(canonicalDetails));
}
}