/**
* 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());
}
}