Files
git.stella-ops.org/src/VexLens/StellaOps.VexLens/Delta/DeltaReport.cs
StellaOps Bot 3098e84de4 save progress
2026-01-04 14:54:52 +02:00

184 lines
5.6 KiB
C#

// Licensed to StellaOps under the AGPL-3.0-or-later license.
using System.Collections.Immutable;
using System.Globalization;
namespace StellaOps.VexLens.Delta;
/// <summary>
/// A report summarizing changes between two vulnerability graph snapshots.
/// </summary>
/// <remarks>
/// DeltaReport groups changes by section for efficient triage:
/// - Users can focus on New findings first
/// - Resolved items can be quickly acknowledged
/// - Confidence changes help reprioritize existing findings
/// - Policy impacts highlight workflow-affecting changes
/// </remarks>
public sealed record DeltaReport
{
/// <summary>
/// Gets the unique identifier for this report.
/// </summary>
public required string ReportId { get; init; }
/// <summary>
/// Gets the digest of the previous snapshot.
/// </summary>
public required string FromSnapshotDigest { get; init; }
/// <summary>
/// Gets the digest of the current snapshot.
/// </summary>
public required string ToSnapshotDigest { get; init; }
/// <summary>
/// Gets the timestamp when the report was generated.
/// </summary>
public required DateTimeOffset GeneratedAt { get; init; }
/// <summary>
/// Gets all delta entries in this report.
/// </summary>
public required ImmutableArray<DeltaEntry> Entries { get; init; }
/// <summary>
/// Gets the summary counts by section.
/// </summary>
public required DeltaSummary Summary { get; init; }
/// <summary>
/// Gets entries grouped by section for UI consumption.
/// </summary>
public ImmutableDictionary<DeltaSection, ImmutableArray<DeltaEntry>> BySection =>
Entries
.GroupBy(e => e.Section)
.ToImmutableDictionary(
g => g.Key,
g => g.ToImmutableArray());
/// <summary>
/// Gets entries for a specific section.
/// </summary>
public ImmutableArray<DeltaEntry> GetSection(DeltaSection section) =>
BySection.TryGetValue(section, out var entries)
? entries
: [];
/// <summary>
/// Gets whether this report has any actionable changes.
/// </summary>
public bool HasActionableChanges =>
Summary.NewCount > 0 ||
Summary.PolicyImpactCount > 0 ||
Summary.ConfidenceUpCount > 0;
/// <summary>
/// Gets a one-line summary suitable for notifications.
/// </summary>
public string ToNotificationSummary()
{
var parts = new List<string>();
if (Summary.NewCount > 0)
{
parts.Add(string.Create(CultureInfo.InvariantCulture,
$"{Summary.NewCount} new"));
}
if (Summary.ResolvedCount > 0)
{
parts.Add(string.Create(CultureInfo.InvariantCulture,
$"{Summary.ResolvedCount} resolved"));
}
if (Summary.PolicyImpactCount > 0)
{
parts.Add(string.Create(CultureInfo.InvariantCulture,
$"{Summary.PolicyImpactCount} policy impact"));
}
if (Summary.ConfidenceUpCount > 0)
{
parts.Add(string.Create(CultureInfo.InvariantCulture,
$"{Summary.ConfidenceUpCount} confidence up"));
}
if (Summary.ConfidenceDownCount > 0)
{
parts.Add(string.Create(CultureInfo.InvariantCulture,
$"{Summary.ConfidenceDownCount} confidence down"));
}
return parts.Count == 0
? "No significant changes"
: string.Join(", ", parts);
}
}
/// <summary>
/// Summary counts for a delta report.
/// </summary>
public sealed record DeltaSummary
{
/// <summary>
/// Gets the total number of entries.
/// </summary>
public required int TotalCount { get; init; }
/// <summary>
/// Gets the count of new findings.
/// </summary>
public required int NewCount { get; init; }
/// <summary>
/// Gets the count of resolved findings.
/// </summary>
public required int ResolvedCount { get; init; }
/// <summary>
/// Gets the count of confidence increases.
/// </summary>
public required int ConfidenceUpCount { get; init; }
/// <summary>
/// Gets the count of confidence decreases.
/// </summary>
public required int ConfidenceDownCount { get; init; }
/// <summary>
/// Gets the count of policy impact changes.
/// </summary>
public required int PolicyImpactCount { get; init; }
/// <summary>
/// Gets the count of damped (suppressed) changes.
/// </summary>
public int DampedCount { get; init; }
/// <summary>
/// Gets the count of evidence-only changes.
/// </summary>
public int EvidenceChangedCount { get; init; }
/// <summary>
/// Creates a summary from a list of entries.
/// </summary>
public static DeltaSummary FromEntries(IEnumerable<DeltaEntry> entries)
{
var list = entries.ToList();
return new DeltaSummary
{
TotalCount = list.Count,
NewCount = list.Count(e => e.Section == DeltaSection.New),
ResolvedCount = list.Count(e => e.Section == DeltaSection.Resolved),
ConfidenceUpCount = list.Count(e => e.Section == DeltaSection.ConfidenceUp),
ConfidenceDownCount = list.Count(e => e.Section == DeltaSection.ConfidenceDown),
PolicyImpactCount = list.Count(e => e.Section == DeltaSection.PolicyImpact),
DampedCount = list.Count(e => e.Section == DeltaSection.Damped),
EvidenceChangedCount = list.Count(e => e.Section == DeltaSection.EvidenceChanged)
};
}
}