using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using StellaOps.Policy.Engine.Domain;
namespace StellaOps.Policy.Engine.Services;
///
/// Deterministic runtime evaluator with per-digest caching.
///
internal sealed class PolicyRuntimeEvaluator
{
private readonly IPolicyPackRepository _repository;
private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal);
public PolicyRuntimeEvaluator(IPolicyPackRepository repository)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
}
public async Task EvaluateAsync(PolicyEvaluationRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
if (string.IsNullOrWhiteSpace(request.PackId))
{
throw new ArgumentException("packId required", nameof(request));
}
if (request.Version <= 0)
{
throw new ArgumentException("version must be positive", nameof(request));
}
if (string.IsNullOrWhiteSpace(request.Subject))
{
throw new ArgumentException("subject required", nameof(request));
}
var bundle = await _repository.GetBundleAsync(request.PackId, request.Version, cancellationToken).ConfigureAwait(false);
if (bundle is null)
{
throw new InvalidOperationException("Bundle not found for requested revision.");
}
var cacheKey = $"{bundle.Digest}|{request.Subject}";
if (_cache.TryGetValue(cacheKey, out var cached))
{
return cached with { Cached = true };
}
var decision = ComputeDecision(bundle.Digest, request.Subject);
var correlationId = ComputeCorrelationId(cacheKey);
var response = new PolicyEvaluationResponse(
request.PackId,
request.Version,
bundle.Digest,
decision,
correlationId,
Cached: false);
_cache.TryAdd(cacheKey, response);
return response;
}
private static string ComputeDecision(string digest, string subject)
{
Span hash = stackalloc byte[32];
SHA256.HashData(Encoding.UTF8.GetBytes($"{digest}|{subject}"), hash);
return (hash[0] & 1) == 0 ? "allow" : "deny";
}
private static string ComputeCorrelationId(string value)
{
Span hash = stackalloc byte[32];
SHA256.HashData(Encoding.UTF8.GetBytes(value), hash);
return Convert.ToHexString(hash);
}
}