// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) StellaOps
using System.Collections.Immutable;
namespace StellaOps.Scanner.Explainability.Assumptions;
///
/// A collection of assumptions associated with a finding or analysis context.
/// Provides methods for querying and validating assumptions.
///
public sealed record AssumptionSet
{
///
/// The unique identifier for this assumption set.
///
public required string Id { get; init; }
///
/// The assumptions in this set, keyed by category and key.
///
public ImmutableArray Assumptions { get; init; } = [];
///
/// When this assumption set was created.
///
public required DateTimeOffset CreatedAt { get; init; }
///
/// Optional context identifier (e.g., finding ID, image digest).
///
public string? ContextId { get; init; }
///
/// Gets all assumptions of a specific category.
///
public IEnumerable GetByCategory(AssumptionCategory category) =>
Assumptions.Where(a => a.Category == category);
///
/// Gets a specific assumption by category and key.
///
public Assumption? Get(AssumptionCategory category, string key) =>
Assumptions.FirstOrDefault(a => a.Category == category &&
string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase));
///
/// Returns the overall confidence level (minimum of all assumptions).
///
public ConfidenceLevel OverallConfidence =>
Assumptions.Length == 0
? ConfidenceLevel.Low
: Assumptions.Min(a => a.Confidence);
///
/// Returns the count of validated assumptions.
///
public int ValidatedCount => Assumptions.Count(a => a.IsValidated);
///
/// Returns the count of contradicted assumptions.
///
public int ContradictedCount => Assumptions.Count(a => a.IsContradicted);
///
/// Returns true if any assumption is contradicted by observed evidence.
///
public bool HasContradictions => Assumptions.Any(a => a.IsContradicted);
///
/// Returns the validation ratio (validated / total with observations).
///
public double ValidationRatio
{
get
{
var withObservations = Assumptions.Count(a => a.ObservedValue is not null);
return withObservations == 0 ? 0.0 : (double)ValidatedCount / withObservations;
}
}
///
/// Creates a new AssumptionSet with an additional assumption.
///
public AssumptionSet WithAssumption(Assumption assumption) =>
this with { Assumptions = Assumptions.Add(assumption) };
///
/// Creates a new AssumptionSet with updated observation for an assumption.
///
public AssumptionSet WithObservation(AssumptionCategory category, string key, string observedValue)
{
var index = Assumptions.FindIndex(a =>
a.Category == category &&
string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase));
if (index < 0)
return this;
var updated = Assumptions[index] with { ObservedValue = observedValue };
return this with { Assumptions = Assumptions.SetItem(index, updated) };
}
}
///
/// Extension methods for ImmutableArray to support FindIndex.
///
internal static class ImmutableArrayExtensions
{
public static int FindIndex(this ImmutableArray array, Func predicate)
{
for (int i = 0; i < array.Length; i++)
{
if (predicate(array[i]))
return i;
}
return -1;
}
}