using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.Provcache;
///
/// Builder for constructing from evaluation results.
/// Ensures deterministic digest computation for cache consistency.
///
public sealed class DecisionDigestBuilder
{
private string? _veriKey;
private string? _verdictHash;
private string? _proofRoot;
private ReplaySeed? _replaySeed;
private DateTimeOffset? _createdAt;
private DateTimeOffset? _expiresAt;
private int? _trustScore;
private TrustScoreBreakdown? _trustScoreBreakdown;
private readonly ProvcacheOptions _options;
private readonly TimeProvider _timeProvider;
///
/// Creates a new DecisionDigestBuilder with default options.
///
public DecisionDigestBuilder() : this(new ProvcacheOptions(), TimeProvider.System)
{
}
///
/// Creates a new DecisionDigestBuilder with the specified options.
///
/// Provcache configuration options.
/// Time provider for timestamps.
public DecisionDigestBuilder(ProvcacheOptions options, TimeProvider? timeProvider = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_timeProvider = timeProvider ?? TimeProvider.System;
}
///
/// Sets the VeriKey for this digest.
///
public DecisionDigestBuilder WithVeriKey(string veriKey)
{
_veriKey = veriKey ?? throw new ArgumentNullException(nameof(veriKey));
return this;
}
///
/// Sets the VeriKey from a builder.
///
public DecisionDigestBuilder WithVeriKey(VeriKeyBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
_veriKey = builder.Build();
return this;
}
///
/// Sets the verdict hash directly.
///
public DecisionDigestBuilder WithVerdictHash(string verdictHash)
{
_verdictHash = verdictHash ?? throw new ArgumentNullException(nameof(verdictHash));
return this;
}
///
/// Computes verdict hash from sorted dispositions.
/// Dispositions are sorted by key for deterministic hashing.
///
/// Dictionary of finding ID to disposition.
public DecisionDigestBuilder WithDispositions(IReadOnlyDictionary dispositions)
{
ArgumentNullException.ThrowIfNull(dispositions);
// Sort by key for deterministic hash
var sorted = dispositions
.OrderBy(kvp => kvp.Key, StringComparer.Ordinal)
.ToList();
if (sorted.Count == 0)
{
_verdictHash = ComputeHash(Encoding.UTF8.GetBytes("empty-verdict"));
return this;
}
// Build deterministic string: key1=value1|key2=value2|...
var sb = new StringBuilder();
foreach (var (key, value) in sorted)
{
if (sb.Length > 0) sb.Append('|');
sb.Append(key);
sb.Append('=');
sb.Append(value);
}
_verdictHash = ComputeHash(Encoding.UTF8.GetBytes(sb.ToString()));
return this;
}
///
/// Sets the proof root (Merkle root of evidence) directly.
///
public DecisionDigestBuilder WithProofRoot(string proofRoot)
{
_proofRoot = proofRoot ?? throw new ArgumentNullException(nameof(proofRoot));
return this;
}
///
/// Computes proof root from a list of evidence chunk hashes.
/// Builds a simple binary Merkle tree for verification.
///
/// Ordered list of evidence chunk hashes.
public DecisionDigestBuilder WithEvidenceChunks(IReadOnlyList evidenceChunkHashes)
{
ArgumentNullException.ThrowIfNull(evidenceChunkHashes);
if (evidenceChunkHashes.Count == 0)
{
_proofRoot = ComputeHash(Encoding.UTF8.GetBytes("empty-proof"));
return this;
}
// Simple Merkle tree: recursively pair and hash until single root
var currentLevel = evidenceChunkHashes
.Select(h => Convert.FromHexString(StripPrefix(h)))
.ToList();
while (currentLevel.Count > 1)
{
var nextLevel = new List();
for (int i = 0; i < currentLevel.Count; i += 2)
{
if (i + 1 < currentLevel.Count)
{
// Hash pair
var combined = new byte[currentLevel[i].Length + currentLevel[i + 1].Length];
currentLevel[i].CopyTo(combined, 0);
currentLevel[i + 1].CopyTo(combined, currentLevel[i].Length);
nextLevel.Add(SHA256.HashData(combined));
}
else
{
// Odd element: promote to next level
nextLevel.Add(currentLevel[i]);
}
}
currentLevel = nextLevel;
}
_proofRoot = $"sha256:{Convert.ToHexStringLower(currentLevel[0])}";
return this;
}
///
/// Sets the replay seed directly.
///
public DecisionDigestBuilder WithReplaySeed(ReplaySeed replaySeed)
{
_replaySeed = replaySeed ?? throw new ArgumentNullException(nameof(replaySeed));
return this;
}
///
/// Builds replay seed from feed and rule identifiers.
///
public DecisionDigestBuilder WithReplaySeed(
IEnumerable feedIds,
IEnumerable ruleIds,
DateTimeOffset? frozenEpoch = null)
{
_replaySeed = new ReplaySeed
{
FeedIds = feedIds?.ToList() ?? [],
RuleIds = ruleIds?.ToList() ?? [],
FrozenEpoch = frozenEpoch
};
return this;
}
///
/// Sets explicit timestamps for created and expires.
///
public DecisionDigestBuilder WithTimestamps(DateTimeOffset createdAt, DateTimeOffset expiresAt)
{
_createdAt = createdAt;
_expiresAt = expiresAt;
return this;
}
///
/// Sets timestamps using the default TTL from options.
///
public DecisionDigestBuilder WithDefaultTimestamps()
{
_createdAt = _timeProvider.GetUtcNow();
_expiresAt = _createdAt.Value.Add(_options.DefaultTtl);
return this;
}
///
/// Sets the trust score directly.
///
/// Trust score (0-100).
public DecisionDigestBuilder WithTrustScore(int trustScore)
{
if (trustScore < 0 || trustScore > 100)
throw new ArgumentOutOfRangeException(nameof(trustScore), "Trust score must be between 0 and 100.");
_trustScore = trustScore;
return this;
}
///
/// Sets the trust score from a breakdown, computing the total automatically.
///
/// The trust score breakdown with component scores.
public DecisionDigestBuilder WithTrustScoreBreakdown(TrustScoreBreakdown breakdown)
{
ArgumentNullException.ThrowIfNull(breakdown);
_trustScoreBreakdown = breakdown;
_trustScore = breakdown.ComputeTotal();
return this;
}
///
/// Computes trust score from component scores using weighted formula,
/// and stores the breakdown for API responses.
///
/// Reachability analysis coverage (0-100).
/// SBOM completeness (0-100).
/// VEX statement coverage (0-100).
/// Policy freshness (0-100).
/// Signer trust level (0-100).
public DecisionDigestBuilder WithTrustScore(
int reachabilityScore,
int sbomCompletenessScore,
int vexCoverageScore,
int policyFreshnessScore,
int signerTrustScore)
{
// Create breakdown with standard weights
_trustScoreBreakdown = TrustScoreBreakdown.CreateDefault(
reachabilityScore,
sbomCompletenessScore,
vexCoverageScore,
policyFreshnessScore,
signerTrustScore);
// Compute total from breakdown
_trustScore = _trustScoreBreakdown.ComputeTotal();
// Clamp to valid range
_trustScore = Math.Clamp(_trustScore.Value, 0, 100);
return this;
}
///
/// Builds the final DecisionDigest.
///
/// The constructed DecisionDigest.
/// If required components are missing.
public DecisionDigest Build()
{
ValidateRequiredComponents();
return new DecisionDigest
{
DigestVersion = _options.DigestVersion,
VeriKey = _veriKey!,
VerdictHash = _verdictHash!,
ProofRoot = _proofRoot!,
ReplaySeed = _replaySeed!,
CreatedAt = _createdAt!.Value,
ExpiresAt = _expiresAt!.Value,
TrustScore = _trustScore!.Value,
TrustScoreBreakdown = _trustScoreBreakdown
};
}
///
/// Resets the builder to its initial state.
///
public DecisionDigestBuilder Reset()
{
_veriKey = null;
_verdictHash = null;
_proofRoot = null;
_replaySeed = null;
_createdAt = null;
_expiresAt = null;
_trustScore = null;
_trustScoreBreakdown = null;
return this;
}
private void ValidateRequiredComponents()
{
var missing = new List();
if (string.IsNullOrWhiteSpace(_veriKey))
missing.Add("VeriKey");
if (string.IsNullOrWhiteSpace(_verdictHash))
missing.Add("VerdictHash");
if (string.IsNullOrWhiteSpace(_proofRoot))
missing.Add("ProofRoot");
if (_replaySeed is null)
missing.Add("ReplaySeed");
if (!_createdAt.HasValue)
missing.Add("CreatedAt");
if (!_expiresAt.HasValue)
missing.Add("ExpiresAt");
if (!_trustScore.HasValue)
missing.Add("TrustScore");
if (missing.Count > 0)
{
throw new InvalidOperationException(
$"Cannot build DecisionDigest: missing required components: {string.Join(", ", missing)}. " +
"Use the With* methods to set all required components before calling Build().");
}
}
private static string ComputeHash(ReadOnlySpan data)
{
Span hash = stackalloc byte[32];
SHA256.HashData(data, hash);
return $"sha256:{Convert.ToHexStringLower(hash)}";
}
private static string StripPrefix(string hash)
{
if (hash.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
return hash[7..];
return hash;
}
}