AnalysisScoreSemanticsFingerprint.java
package com.taxonomy.dto;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Map;
/** Creates a stable SHA-256 digest of the complete typed analysis-score model. */
public final class AnalysisScoreSemanticsFingerprint {
private static final String VERSION = "analysis-score-semantics-v1";
private AnalysisScoreSemanticsFingerprint() {
}
/**
* Hashes node identity, score kind, raw evidence, effective relevance and parent evidence.
* Map iteration order has no effect on the result.
*/
public static String sha256(Map<String, AnalysisScoreDetail> scoreDetails) {
MessageDigest digest = sha256Digest();
update(digest, VERSION);
if (scoreDetails != null) {
scoreDetails.entrySet().stream()
.filter(entry -> entry.getKey() != null && entry.getValue() != null)
.sorted(Map.Entry.comparingByKey())
.forEach(entry -> updateDetail(digest, entry.getKey(), entry.getValue()));
}
return HexFormat.of().formatHex(digest.digest());
}
/**
* Extends an existing analysis digest with the typed score contract. Legacy reports without
* score details retain their original digest; typed reports receive a deterministic digest
* that changes whenever any score meaning or evidence changes.
*/
public static String extend(
String baseFingerprint,
Map<String, AnalysisScoreDetail> scoreDetails) {
if (scoreDetails == null || scoreDetails.isEmpty()) {
return baseFingerprint;
}
MessageDigest digest = sha256Digest();
update(digest, VERSION);
update(digest, safe(baseFingerprint));
update(digest, sha256(scoreDetails));
return HexFormat.of().formatHex(digest.digest());
}
private static void updateDetail(
MessageDigest digest,
String mapCode,
AnalysisScoreDetail detail) {
String canonical = String.join("\u001f",
safe(mapCode).strip(),
safe(detail.nodeCode()),
detail.kind().name(),
Integer.toString(detail.rawScore()),
Integer.toString(detail.effectiveRelevance()),
safe(detail.parentCode()),
detail.parentScore() == null ? "" : detail.parentScore().toString());
update(digest, canonical);
}
private static MessageDigest sha256Digest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException impossible) {
throw new IllegalStateException("SHA-256 is required by the Java platform", impossible);
}
}
private static void update(MessageDigest digest, String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
digest.update((byte) (bytes.length >>> 24));
digest.update((byte) (bytes.length >>> 16));
digest.update((byte) (bytes.length >>> 8));
digest.update((byte) bytes.length);
digest.update(bytes);
}
private static String safe(String value) {
return value == null ? "" : value;
}
}