/** * Verdict Delta Detection * Sprint: SPRINT_9100_0002_0002 (Per-Node VerdictDigest) * Tasks: VDIGEST-9100-006 through VDIGEST-9100-015 * * Provides delta detection between resolution results. */ using System.Collections.Immutable; namespace StellaOps.Resolver; /// /// Delta between two resolution results at the verdict level. /// /// Verdicts where the digest changed (same node, different verdict). /// Verdicts for nodes that are only in the new result. /// Verdicts for nodes that are only in the old result. public sealed record VerdictDelta( ImmutableArray<(Verdict Old, Verdict New)> ChangedVerdicts, ImmutableArray AddedVerdicts, ImmutableArray RemovedVerdicts) { /// /// Returns true if there are no differences. /// public bool IsEmpty => ChangedVerdicts.IsEmpty && AddedVerdicts.IsEmpty && RemovedVerdicts.IsEmpty; } /// /// Interface for detecting verdict deltas. /// public interface IVerdictDeltaDetector { /// /// Detects differences between two resolution results. /// VerdictDelta Detect(ResolutionResult old, ResolutionResult @new); } /// /// Default verdict delta detector. /// public sealed class DefaultVerdictDeltaDetector : IVerdictDeltaDetector { public VerdictDelta Detect(ResolutionResult old, ResolutionResult @new) { ArgumentNullException.ThrowIfNull(old); ArgumentNullException.ThrowIfNull(@new); var oldVerdicts = old.Verdicts.ToDictionary(v => v.Node); var newVerdicts = @new.Verdicts.ToDictionary(v => v.Node); var changed = new List<(Verdict Old, Verdict New)>(); var added = new List(); var removed = new List(); // Find changed and removed foreach (var (nodeId, oldVerdict) in oldVerdicts) { if (newVerdicts.TryGetValue(nodeId, out var newVerdict)) { if (oldVerdict.VerdictDigest != newVerdict.VerdictDigest) { changed.Add((oldVerdict, newVerdict)); } } else { removed.Add(oldVerdict); } } // Find added foreach (var (nodeId, newVerdict) in newVerdicts) { if (!oldVerdicts.ContainsKey(nodeId)) { added.Add(newVerdict); } } return new VerdictDelta( changed.ToImmutableArray(), added.ToImmutableArray(), removed.ToImmutableArray()); } } /// /// Human-readable diff report for verdict changes. /// public sealed record VerdictDiffReport( ImmutableArray Entries); /// /// Single entry in a verdict diff report. /// /// The node that changed. /// Type of change (Changed, Added, Removed). /// Old verdict status (if applicable). /// New verdict status (if applicable). /// Old verdict digest. /// New verdict digest. public sealed record VerdictDiffEntry( string NodeId, string ChangeType, string? OldStatus, string? NewStatus, string? OldDigest, string? NewDigest); /// /// Interface for generating verdict diff reports. /// public interface IVerdictDiffReporter { /// /// Generates a diff report from a verdict delta. /// VerdictDiffReport GenerateReport(VerdictDelta delta); } /// /// Default verdict diff reporter. /// public sealed class DefaultVerdictDiffReporter : IVerdictDiffReporter { public VerdictDiffReport GenerateReport(VerdictDelta delta) { var entries = new List(); foreach (var (old, @new) in delta.ChangedVerdicts) { entries.Add(new VerdictDiffEntry( old.Node.Value, "Changed", old.Status.ToString(), @new.Status.ToString(), old.VerdictDigest, @new.VerdictDigest)); } foreach (var added in delta.AddedVerdicts) { entries.Add(new VerdictDiffEntry( added.Node.Value, "Added", null, added.Status.ToString(), null, added.VerdictDigest)); } foreach (var removed in delta.RemovedVerdicts) { entries.Add(new VerdictDiffEntry( removed.Node.Value, "Removed", removed.Status.ToString(), null, removed.VerdictDigest, null)); } // Sort by NodeId for determinism entries.Sort((a, b) => string.Compare(a.NodeId, b.NodeId, StringComparison.Ordinal)); return new VerdictDiffReport(entries.ToImmutableArray()); } }