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