Files
git.stella-ops.org/src/__Libraries/StellaOps.ReachGraph/Deduplication/DeduplicatedEdge.cs
StellaOps Bot 3098e84de4 save progress
2026-01-04 14:54:52 +02:00

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