using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.Policy.Engine.AdvisoryAI;
///
/// In-memory store for Advisory AI knobs (POLICY-ENGINE-31-001).
///
internal sealed class AdvisoryAiKnobsService
{
private readonly TimeProvider _timeProvider;
private readonly object _lock = new();
private AdvisoryAiKnobsProfile _current;
public AdvisoryAiKnobsService(TimeProvider timeProvider)
{
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_current = BuildProfile(DefaultKnobs());
}
public AdvisoryAiKnobsProfile Get() => _current;
public AdvisoryAiKnobsProfile Set(IReadOnlyList knobs)
{
var normalized = Normalize(knobs);
var profile = BuildProfile(normalized);
lock (_lock)
{
_current = profile;
}
return profile;
}
private AdvisoryAiKnobsProfile BuildProfile(IReadOnlyList knobs)
{
var json = JsonSerializer.Serialize(knobs, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
});
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json)));
return new AdvisoryAiKnobsProfile(knobs, hash);
}
private IReadOnlyList Normalize(IReadOnlyList knobs)
{
var normalized = knobs
.Where(k => !string.IsNullOrWhiteSpace(k.Name))
.Select(k => new AdvisoryAiKnob(
Name: k.Name.Trim().ToLowerInvariant(),
DefaultValue: k.DefaultValue,
Min: k.Min,
Max: k.Max,
Step: k.Step <= 0 ? 0.001m : k.Step,
Description: string.IsNullOrWhiteSpace(k.Description) ? string.Empty : k.Description.Trim()))
.OrderBy(k => k.Name, StringComparer.Ordinal)
.ToList();
return normalized;
}
private static IReadOnlyList DefaultKnobs() =>
new[]
{
new AdvisoryAiKnob("ai_signal_weight", 1.0m, 0m, 2m, 0.01m, "Weight applied to AI signals"),
new AdvisoryAiKnob("reachability_boost", 0.2m, 0m, 1m, 0.01m, "Boost when asset is reachable"),
new AdvisoryAiKnob("time_decay_half_life_days", 30m, 1m, 365m, 1m, "Half-life for decay"),
new AdvisoryAiKnob("evidence_freshness_threshold_hours", 72m, 1m, 720m, 1m, "Max evidence age")
};
}