// Licensed to StellaOps under the AGPL-3.0-or-later license. using System.Collections.Immutable; using StellaOps.ReachGraph.Schema; namespace StellaOps.ReachGraph.Deduplication; /// /// An edge that has been deduplicated from multiple source edges. /// Preserves provenance by tracking all contributing sources. /// public sealed record DeduplicatedEdge { /// /// Gets the semantic key for this edge. /// public required EdgeSemanticKey Key { get; init; } /// /// Gets the source node ID (from entry point). /// public required string From { get; init; } /// /// Gets the target node ID (to sink). /// public required string To { get; init; } /// /// Gets the aggregated explanation for this edge. /// public required EdgeExplanation Why { get; init; } /// /// Gets the set of source identifiers that contributed this edge. /// public required ImmutableHashSet Sources { get; init; } /// /// Gets the maximum strength (weight) among all contributing sources. /// public required double Strength { get; init; } /// /// Gets the timestamp of the most recent observation of this edge. /// public required DateTimeOffset LastSeen { get; init; } /// /// Gets the number of contributing sources. /// public int SourceCount => Sources.Count; /// /// Gets whether this edge has multiple confirming sources. /// public bool IsCorroborated => Sources.Count > 1; } /// /// Builder for creating instances by merging multiple source edges. /// public sealed class DeduplicatedEdgeBuilder { private readonly EdgeSemanticKey _key; private readonly string _from; private readonly string _to; private readonly HashSet _sources = new(StringComparer.Ordinal); private EdgeExplanation? _explanation; private double _maxStrength; private DateTimeOffset _lastSeen = DateTimeOffset.MinValue; /// /// Initializes a new builder for the given semantic key. /// public DeduplicatedEdgeBuilder(EdgeSemanticKey key, string from, string to) { _key = key; _from = from; _to = to; } /// /// Adds a source edge to this builder. /// /// The source identifier (e.g., feed name, analyzer ID). /// The edge explanation from this source. /// The strength/weight from this source. /// When this source observed the edge. /// This builder for chaining. public DeduplicatedEdgeBuilder AddSource( string sourceId, EdgeExplanation explanation, double strength, DateTimeOffset observedAt) { _sources.Add(sourceId); // Keep the strongest explanation if (strength > _maxStrength || _explanation is null) { _maxStrength = strength; _explanation = explanation; } // Track most recent observation if (observedAt > _lastSeen) { _lastSeen = observedAt; } return this; } /// /// Builds the deduplicated edge. /// /// The deduplicated edge with merged provenance. /// If no sources were added. public DeduplicatedEdge Build() { if (_sources.Count == 0 || _explanation is null) { throw new InvalidOperationException("At least one source must be added before building."); } return new DeduplicatedEdge { Key = _key, From = _from, To = _to, Why = _explanation, Sources = _sources.ToImmutableHashSet(StringComparer.Ordinal), Strength = _maxStrength, LastSeen = _lastSeen }; } }