112 lines
3.3 KiB
C#
112 lines
3.3 KiB
C#
/**
|
|
* 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;
|
|
|
|
/// <summary>
|
|
/// Delta between two graphs at the edge level.
|
|
/// </summary>
|
|
/// <param name="AddedEdges">Edges present in new graph but not in old.</param>
|
|
/// <param name="RemovedEdges">Edges present in old graph but not in new.</param>
|
|
/// <param name="ModifiedEdges">Edges with same (src, kind, dst) but different attributes.</param>
|
|
public sealed record EdgeDelta(
|
|
ImmutableArray<Edge> AddedEdges,
|
|
ImmutableArray<Edge> RemovedEdges,
|
|
ImmutableArray<(Edge Old, Edge New)> ModifiedEdges)
|
|
{
|
|
/// <summary>
|
|
/// Returns true if there are no differences.
|
|
/// </summary>
|
|
public bool IsEmpty => AddedEdges.IsEmpty && RemovedEdges.IsEmpty && ModifiedEdges.IsEmpty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Interface for detecting edge deltas.
|
|
/// </summary>
|
|
public interface IEdgeDeltaDetector
|
|
{
|
|
/// <summary>
|
|
/// Detects differences between two graphs at the edge level.
|
|
/// </summary>
|
|
EdgeDelta Detect(EvidenceGraph old, EvidenceGraph @new);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Default edge delta detector.
|
|
/// </summary>
|
|
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<Edge>();
|
|
var removed = new List<Edge>();
|
|
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();
|
|
}
|
|
}
|