AnalysisResult.java
package com.taxonomy.dto;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class AnalysisResult {
/**
* Original node scores returned by analysis. Concrete PRODUCT entries are independent
* suitability values and are retained separately from comparable relevance.
*/
private Map<String, Integer> rawScores = new LinkedHashMap<>();
/**
* True after a constructor or setter supplied non-null {@code rawScores}, even an empty
* map. It makes JSON deserialization order-independent when both legacy {@code scores} and {@code rawScores}
* are present.
*/
private transient boolean explicitRawScores;
/** Whether the current raw evidence came from the legacy JSON setter. */
private transient boolean legacyScoresSupplied;
/** Taxonomy-tree instance used to derive the currently cached score semantics. */
private transient List<TaxonomyNodeDto> scoreSemanticsTree;
/** Version of the explicit score-semantics envelope. */
private int scoreSemanticsVersion;
/** SHA-256 of raw, effective, kind and parent score evidence. */
private String scoreSemanticsFingerprintSha256;
/** Node code → typed interpretation of the original score. */
private Map<String, AnalysisScoreDetail> scoreDetails = new LinkedHashMap<>();
/** Node code → comparable 0–100 relevance used by generic downstream consumers. */
private Map<String, Integer> effectiveScores = new LinkedHashMap<>();
/** Concrete product code → original independent 0–100 suitability. */
private Map<String, Integer> productSuitabilityScores = new LinkedHashMap<>();
/** Bounded compatibility warnings produced while deriving semantics for legacy payloads. */
private List<String> scoreSemanticsWarnings = new ArrayList<>();
/** Original node-code → reason text returned by the scoring provider. */
private Map<String, String> reasons = new LinkedHashMap<>();
/** Effective provider used for this analysis (informational/provenance). */
private String provider;
private List<TaxonomyNodeDto> tree;
/** "SUCCESS", "PARTIAL", or "ERROR" */
private String status;
/** Accumulated warning messages (e.g. which roots were skipped). */
private List<String> warnings = new ArrayList<>();
/** Human-readable error/partial message; set when status is PARTIAL or ERROR. */
private String errorMessage;
/** Optional architecture view built from relation-aware propagation. */
private RequirementArchitectureView architectureView;
/**
* Scoring discrepancies detected during analysis. Each entry indicates
* a case where the LLM's raw child scores exceeded the parent's budget,
* signalling a potential taxonomy inconsistency.
*/
private List<TaxonomyDiscrepancy> discrepancies = new ArrayList<>();
/** Relevant product families for which no catalogued product met the suitability threshold. */
private List<ProductCoverageGap> productCoverageGaps = new ArrayList<>();
/** Provisional (not-yet-persisted) relation hypotheses generated from scores. */
private List<RelationHypothesisDto> provisionalRelations = new ArrayList<>();
/** Git commit provenance and projection/index freshness metadata. */
private ViewContext viewContext;
public AnalysisResult() {}
public AnalysisResult(Map<String, Integer> rawScores, List<TaxonomyNodeDto> tree) {
setRawScores(rawScores);
this.tree = tree;
refreshScoreSemantics();
}
/**
* Returns comparable relevance values. Existing generic consumers therefore cannot silently
* rank conditional product suitability as though it were absolute hierarchical relevance.
* Use {@link #getRawScores()} or {@link #getProductSuitabilityScores()} for scoring evidence.
*/
public Map<String, Integer> getScores() {
ensureScoreSemantics();
return effectiveScores;
}
/**
* Legacy JSON input. Old payloads contain only {@code scores}; new payloads also contain
* {@code rawScores}, which takes precedence independently of property order.
*/
public void setScores(Map<String, Integer> scores) {
if (!explicitRawScores) {
this.rawScores = normalizeRawScores(scores);
this.legacyScoresSupplied = true;
}
}
public Map<String, Integer> getRawScores() {
return rawScores;
}
/**
* Non-null raw evidence always wins over the legacy alias, including an explicit empty map.
* Null means no new-format evidence: preserve an accepted legacy input, or clear a previous
* constructor/explicit raw value. This also covers property-based JSON construction.
*/
public void setRawScores(Map<String, Integer> rawScores) {
if (rawScores == null && legacyScoresSupplied) {
return;
}
this.rawScores = normalizeRawScores(rawScores);
this.explicitRawScores = rawScores != null;
this.legacyScoresSupplied = false;
}
public int getScoreSemanticsVersion() {
ensureScoreSemantics();
return scoreSemanticsVersion;
}
public void setScoreSemanticsVersion(int scoreSemanticsVersion) {
this.scoreSemanticsVersion = scoreSemanticsVersion;
}
public String getScoreSemanticsFingerprintSha256() {
ensureScoreSemantics();
return scoreSemanticsFingerprintSha256;
}
public void setScoreSemanticsFingerprintSha256(String scoreSemanticsFingerprintSha256) {
this.scoreSemanticsFingerprintSha256 = scoreSemanticsFingerprintSha256;
}
public Map<String, AnalysisScoreDetail> getScoreDetails() {
ensureScoreSemantics();
return scoreDetails;
}
public void setScoreDetails(Map<String, AnalysisScoreDetail> scoreDetails) {
this.scoreDetails = scoreDetails != null
? new LinkedHashMap<>(scoreDetails) : new LinkedHashMap<>();
}
public Map<String, Integer> getEffectiveScores() {
ensureScoreSemantics();
return effectiveScores;
}
public void setEffectiveScores(Map<String, Integer> effectiveScores) {
this.effectiveScores = effectiveScores != null
? new LinkedHashMap<>(effectiveScores) : new LinkedHashMap<>();
}
public Map<String, Integer> getProductSuitabilityScores() {
ensureScoreSemantics();
return productSuitabilityScores;
}
public void setProductSuitabilityScores(Map<String, Integer> productSuitabilityScores) {
this.productSuitabilityScores = productSuitabilityScores != null
? new LinkedHashMap<>(productSuitabilityScores) : new LinkedHashMap<>();
}
public List<String> getScoreSemanticsWarnings() {
ensureScoreSemantics();
return scoreSemanticsWarnings;
}
public void setScoreSemanticsWarnings(List<String> scoreSemanticsWarnings) {
this.scoreSemanticsWarnings = scoreSemanticsWarnings != null
? new ArrayList<>(scoreSemanticsWarnings) : new ArrayList<>();
}
/** Rebuilds all derived score views from the original scores and frozen taxonomy tree. */
public final void refreshScoreSemantics() {
rawScores = normalizeRawScores(rawScores);
AnalysisScoreSemantics.Derived derived = AnalysisScoreSemantics.derive(rawScores, tree);
scoreSemanticsVersion = AnalysisScoreSemantics.CURRENT_VERSION;
scoreDetails = new LinkedHashMap<>(derived.scoreDetails());
effectiveScores = new LinkedHashMap<>(derived.effectiveScores());
productSuitabilityScores = new LinkedHashMap<>(derived.productSuitabilityScores());
scoreSemanticsWarnings = new ArrayList<>(derived.warnings());
scoreSemanticsFingerprintSha256 =
AnalysisScoreSemanticsFingerprint.sha256(scoreDetails);
scoreSemanticsTree = tree;
}
private static Map<String, Integer> normalizeRawScores(Map<String, Integer> scores) {
return new LinkedHashMap<>(AnalysisScoreSemantics.normalizeScores(scores));
}
private void ensureScoreSemantics() {
// Empty or absent raw evidence must invalidate stale derived views too.
if (rawScores == null || !hasCompleteScoreSemantics()) {
refreshScoreSemantics();
}
}
private boolean hasCompleteScoreSemantics() {
if (scoreSemanticsTree != tree
|| scoreSemanticsVersion != AnalysisScoreSemantics.CURRENT_VERSION
|| scoreDetails == null || effectiveScores == null
|| productSuitabilityScores == null
|| !scoreDetails.keySet().equals(rawScores.keySet())
|| !effectiveScores.keySet().equals(rawScores.keySet())) {
return false;
}
int productCount = 0;
for (Map.Entry<String, Integer> entry : rawScores.entrySet()) {
AnalysisScoreDetail detail = scoreDetails.get(entry.getKey());
Integer effective = effectiveScores.get(entry.getKey());
if (entry.getValue() == null || detail == null || effective == null
|| !entry.getKey().equals(detail.nodeCode())
|| detail.rawScore() != Math.max(0, Math.min(100, entry.getValue()))
|| detail.effectiveRelevance() != effective) {
return false;
}
if (detail.isProductSuitability()) {
productCount++;
if (!Objects.equals(productSuitabilityScores.get(entry.getKey()), detail.rawScore())) {
return false;
}
}
}
return productSuitabilityScores.size() == productCount
&& Objects.equals(scoreSemanticsFingerprintSha256,
AnalysisScoreSemanticsFingerprint.sha256(scoreDetails));
}
public Map<String, String> getReasons() { return reasons; }
public void setReasons(Map<String, String> reasons) {
this.reasons = reasons != null ? reasons : new LinkedHashMap<>();
}
public String getProvider() { return provider; }
public void setProvider(String provider) { this.provider = provider; }
public List<TaxonomyNodeDto> getTree() { return tree; }
public void setTree(List<TaxonomyNodeDto> tree) {
this.tree = tree;
this.scoreSemanticsTree = null;
this.scoreSemanticsVersion = 0;
this.scoreSemanticsFingerprintSha256 = null;
}
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public List<String> getWarnings() { return warnings; }
public void setWarnings(List<String> warnings) {
this.warnings = warnings != null ? warnings : new ArrayList<>();
}
public String getErrorMessage() { return errorMessage; }
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
public RequirementArchitectureView getArchitectureView() { return architectureView; }
public void setArchitectureView(RequirementArchitectureView architectureView) { this.architectureView = architectureView; }
public List<TaxonomyDiscrepancy> getDiscrepancies() { return discrepancies; }
public void setDiscrepancies(List<TaxonomyDiscrepancy> discrepancies) {
this.discrepancies = discrepancies != null ? discrepancies : new ArrayList<>();
}
public List<ProductCoverageGap> getProductCoverageGaps() { return productCoverageGaps; }
public void setProductCoverageGaps(List<ProductCoverageGap> productCoverageGaps) {
this.productCoverageGaps = productCoverageGaps != null
? productCoverageGaps : new ArrayList<>();
}
public List<RelationHypothesisDto> getProvisionalRelations() { return provisionalRelations; }
public void setProvisionalRelations(List<RelationHypothesisDto> provisionalRelations) {
this.provisionalRelations = provisionalRelations != null
? provisionalRelations : new ArrayList<>();
}
public ViewContext getViewContext() { return viewContext; }
public void setViewContext(ViewContext viewContext) { this.viewContext = viewContext; }
}