// 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
};
}
}