/** * Edge Delta Detection * Sprint: SPRINT_9100_0001_0003 (Content-Addressed EdgeId) * Tasks: EDGEID-9100-012 through EDGEID-9100-014 * * Provides delta detection between evidence graphs at the edge level. */ using System.Collections.Immutable; using System.Security.Cryptography; using System.Text; using System.Text.Json; namespace StellaOps.Resolver; /// /// Delta between two graphs at the edge level. /// /// Edges present in new graph but not in old. /// Edges present in old graph but not in new. /// Edges with same (src, kind, dst) but different attributes. public sealed record EdgeDelta( ImmutableArray AddedEdges, ImmutableArray RemovedEdges, ImmutableArray<(Edge Old, Edge New)> ModifiedEdges) { /// /// Returns true if there are no differences. /// public bool IsEmpty => AddedEdges.IsEmpty && RemovedEdges.IsEmpty && ModifiedEdges.IsEmpty; } /// /// Interface for detecting edge deltas. /// public interface IEdgeDeltaDetector { /// /// Detects differences between two graphs at the edge level. /// EdgeDelta Detect(EvidenceGraph old, EvidenceGraph @new); } /// /// Default edge delta detector. /// public sealed class DefaultEdgeDeltaDetector : IEdgeDeltaDetector { public EdgeDelta Detect(EvidenceGraph old, EvidenceGraph @new) { ArgumentNullException.ThrowIfNull(old); ArgumentNullException.ThrowIfNull(@new); // Group edges by their identity (EdgeId), which is based on (src, kind, dst) var oldEdges = old.Edges.ToDictionary(e => e.Id); var newEdges = @new.Edges.ToDictionary(e => e.Id); var added = new List(); var removed = new List(); var modified = new List<(Edge Old, Edge New)>(); // Find added and modified foreach (var (edgeId, newEdge) in newEdges) { if (oldEdges.TryGetValue(edgeId, out var oldEdge)) { // Same EdgeId - check if attributes changed if (!AttributesEqual(oldEdge.Attrs, newEdge.Attrs)) { modified.Add((oldEdge, newEdge)); } } else { added.Add(newEdge); } } // Find removed foreach (var (edgeId, oldEdge) in oldEdges) { if (!newEdges.ContainsKey(edgeId)) { removed.Add(oldEdge); } } return new EdgeDelta( added.ToImmutableArray(), removed.ToImmutableArray(), modified.ToImmutableArray()); } private static bool AttributesEqual(JsonElement? a, JsonElement? b) { if (a is null && b is null) return true; if (a is null || b is null) return false; var aHash = ComputeAttrsHash(a.Value); var bHash = ComputeAttrsHash(b.Value); return aHash == bHash; } private static string ComputeAttrsHash(JsonElement attrs) { var json = attrs.GetRawText(); var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json)); return Convert.ToHexString(hash).ToLowerInvariant(); } }