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