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") }; }