sprints and audit work
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Context for determinization evaluation.
|
||||
/// Contains environment, criticality, and policy settings.
|
||||
/// </summary>
|
||||
public sealed record DeterminizationContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Deployment environment.
|
||||
/// </summary>
|
||||
[JsonPropertyName("environment")]
|
||||
public required DeploymentEnvironment Environment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Asset criticality level.
|
||||
/// </summary>
|
||||
[JsonPropertyName("criticality")]
|
||||
public required AssetCriticality Criticality { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Entropy threshold for this context.
|
||||
/// Observations above this trigger guardrails.
|
||||
/// </summary>
|
||||
[JsonPropertyName("entropy_threshold")]
|
||||
public required double EntropyThreshold { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Decay threshold for this context.
|
||||
/// Observations below this are considered stale.
|
||||
/// </summary>
|
||||
[JsonPropertyName("decay_threshold")]
|
||||
public required double DecayThreshold { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates context with default production settings.
|
||||
/// </summary>
|
||||
public static DeterminizationContext Production() => new()
|
||||
{
|
||||
Environment = DeploymentEnvironment.Production,
|
||||
Criticality = AssetCriticality.High,
|
||||
EntropyThreshold = 0.4,
|
||||
DecayThreshold = 0.50
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates context with relaxed development settings.
|
||||
/// </summary>
|
||||
public static DeterminizationContext Development() => new()
|
||||
{
|
||||
Environment = DeploymentEnvironment.Development,
|
||||
Criticality = AssetCriticality.Low,
|
||||
EntropyThreshold = 0.6,
|
||||
DecayThreshold = 0.35
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates context with custom thresholds.
|
||||
/// </summary>
|
||||
public static DeterminizationContext Create(
|
||||
DeploymentEnvironment environment,
|
||||
AssetCriticality criticality,
|
||||
double entropyThreshold,
|
||||
double decayThreshold) => new()
|
||||
{
|
||||
Environment = environment,
|
||||
Criticality = criticality,
|
||||
EntropyThreshold = entropyThreshold,
|
||||
DecayThreshold = decayThreshold
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result of determinization evaluation.
|
||||
/// Combines observation state, uncertainty score, and guardrails.
|
||||
/// </summary>
|
||||
public sealed record DeterminizationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Resulting observation state.
|
||||
/// </summary>
|
||||
[JsonPropertyName("state")]
|
||||
public required ObservationState State { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Uncertainty score at evaluation time.
|
||||
/// </summary>
|
||||
[JsonPropertyName("uncertainty")]
|
||||
public required UncertaintyScore Uncertainty { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Decay status at evaluation time.
|
||||
/// </summary>
|
||||
[JsonPropertyName("decay")]
|
||||
public required ObservationDecay Decay { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Applied guardrails (if any).
|
||||
/// </summary>
|
||||
[JsonPropertyName("guardrails")]
|
||||
public GuardRails? Guardrails { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evaluation context.
|
||||
/// </summary>
|
||||
[JsonPropertyName("context")]
|
||||
public required DeterminizationContext Context { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When this result was computed (UTC).
|
||||
/// </summary>
|
||||
[JsonPropertyName("evaluated_at")]
|
||||
public required DateTimeOffset EvaluatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Decision rationale.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rationale")]
|
||||
public string? Rationale { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates result for determined observation (low uncertainty).
|
||||
/// </summary>
|
||||
public static DeterminizationResult Determined(
|
||||
UncertaintyScore uncertainty,
|
||||
ObservationDecay decay,
|
||||
DeterminizationContext context,
|
||||
DateTimeOffset evaluatedAt) => new()
|
||||
{
|
||||
State = ObservationState.Determined,
|
||||
Uncertainty = uncertainty,
|
||||
Decay = decay,
|
||||
Guardrails = GuardRails.None(),
|
||||
Context = context,
|
||||
EvaluatedAt = evaluatedAt,
|
||||
Rationale = "Evidence sufficient for confident determination"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates result for pending observation (high uncertainty).
|
||||
/// </summary>
|
||||
public static DeterminizationResult Pending(
|
||||
UncertaintyScore uncertainty,
|
||||
ObservationDecay decay,
|
||||
GuardRails guardrails,
|
||||
DeterminizationContext context,
|
||||
DateTimeOffset evaluatedAt) => new()
|
||||
{
|
||||
State = ObservationState.PendingDeterminization,
|
||||
Uncertainty = uncertainty,
|
||||
Decay = decay,
|
||||
Guardrails = guardrails,
|
||||
Context = context,
|
||||
EvaluatedAt = evaluatedAt,
|
||||
Rationale = $"Uncertainty ({uncertainty.Entropy:F2}) above threshold ({context.EntropyThreshold:F2})"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates result for stale observation requiring refresh.
|
||||
/// </summary>
|
||||
public static DeterminizationResult Stale(
|
||||
UncertaintyScore uncertainty,
|
||||
ObservationDecay decay,
|
||||
DeterminizationContext context,
|
||||
DateTimeOffset evaluatedAt) => new()
|
||||
{
|
||||
State = ObservationState.StaleRequiresRefresh,
|
||||
Uncertainty = uncertainty,
|
||||
Decay = decay,
|
||||
Guardrails = GuardRails.Strict(),
|
||||
Context = context,
|
||||
EvaluatedAt = evaluatedAt,
|
||||
Rationale = $"Evidence decayed below threshold ({context.DecayThreshold:F2})"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates result for disputed observation (conflicting signals).
|
||||
/// </summary>
|
||||
public static DeterminizationResult Disputed(
|
||||
UncertaintyScore uncertainty,
|
||||
ObservationDecay decay,
|
||||
DeterminizationContext context,
|
||||
DateTimeOffset evaluatedAt,
|
||||
string reason) => new()
|
||||
{
|
||||
State = ObservationState.Disputed,
|
||||
Uncertainty = uncertainty,
|
||||
Decay = decay,
|
||||
Guardrails = GuardRails.Strict(),
|
||||
Context = context,
|
||||
EvaluatedAt = evaluatedAt,
|
||||
Rationale = $"Conflicting signals detected: {reason}"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Guardrails policy configuration for uncertain observations.
|
||||
/// Defines monitoring/restrictions when evidence is incomplete.
|
||||
/// </summary>
|
||||
public sealed record GuardRails
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable runtime monitoring.
|
||||
/// </summary>
|
||||
[JsonPropertyName("enable_monitoring")]
|
||||
public required bool EnableMonitoring { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Restrict deployment to non-production environments.
|
||||
/// </summary>
|
||||
[JsonPropertyName("restrict_to_non_prod")]
|
||||
public required bool RestrictToNonProd { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Require manual approval before deployment.
|
||||
/// </summary>
|
||||
[JsonPropertyName("require_approval")]
|
||||
public required bool RequireApproval { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Schedule automatic re-evaluation after this duration.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reeval_after")]
|
||||
public TimeSpan? ReevalAfter { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional notes/rationale for guardrails.
|
||||
/// </summary>
|
||||
[JsonPropertyName("notes")]
|
||||
public string? Notes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates GuardRails with default safe settings.
|
||||
/// </summary>
|
||||
public static GuardRails Default() => new()
|
||||
{
|
||||
EnableMonitoring = true,
|
||||
RestrictToNonProd = false,
|
||||
RequireApproval = false,
|
||||
ReevalAfter = TimeSpan.FromDays(7),
|
||||
Notes = null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates GuardRails for high-uncertainty observations.
|
||||
/// </summary>
|
||||
public static GuardRails Strict() => new()
|
||||
{
|
||||
EnableMonitoring = true,
|
||||
RestrictToNonProd = true,
|
||||
RequireApproval = true,
|
||||
ReevalAfter = TimeSpan.FromDays(3),
|
||||
Notes = "High uncertainty - strict guardrails applied"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates GuardRails with no restrictions (all evidence present).
|
||||
/// </summary>
|
||||
public static GuardRails None() => new()
|
||||
{
|
||||
EnableMonitoring = false,
|
||||
RestrictToNonProd = false,
|
||||
RequireApproval = false,
|
||||
ReevalAfter = null,
|
||||
Notes = null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deployment environment classification.
|
||||
/// </summary>
|
||||
public enum DeploymentEnvironment
|
||||
{
|
||||
/// <summary>Development environment.</summary>
|
||||
Development = 0,
|
||||
|
||||
/// <summary>Testing environment.</summary>
|
||||
Testing = 1,
|
||||
|
||||
/// <summary>Staging/pre-production environment.</summary>
|
||||
Staging = 2,
|
||||
|
||||
/// <summary>Production environment.</summary>
|
||||
Production = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asset criticality classification.
|
||||
/// </summary>
|
||||
public enum AssetCriticality
|
||||
{
|
||||
/// <summary>Low criticality - minimal impact if compromised.</summary>
|
||||
Low = 0,
|
||||
|
||||
/// <summary>Medium criticality - moderate impact.</summary>
|
||||
Medium = 1,
|
||||
|
||||
/// <summary>High criticality - significant impact.</summary>
|
||||
High = 2,
|
||||
|
||||
/// <summary>Critical - severe impact if compromised.</summary>
|
||||
Critical = 3
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Per-observation decay configuration.
|
||||
/// Tracks evidence staleness with configurable half-life.
|
||||
/// Formula: decayed = max(floor, exp(-ln(2) * age_days / half_life_days))
|
||||
/// </summary>
|
||||
public sealed record ObservationDecay
|
||||
{
|
||||
/// <summary>
|
||||
/// When the observation was first recorded (UTC).
|
||||
/// </summary>
|
||||
[JsonPropertyName("observed_at")]
|
||||
public required DateTimeOffset ObservedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the observation was last refreshed (UTC).
|
||||
/// </summary>
|
||||
[JsonPropertyName("refreshed_at")]
|
||||
public required DateTimeOffset RefreshedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Half-life in days.
|
||||
/// Default: 14 days.
|
||||
/// </summary>
|
||||
[JsonPropertyName("half_life_days")]
|
||||
public required double HalfLifeDays { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum confidence floor.
|
||||
/// Default: 0.35 (consistent with FreshnessCalculator).
|
||||
/// </summary>
|
||||
[JsonPropertyName("floor")]
|
||||
public required double Floor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Staleness threshold (0.0-1.0).
|
||||
/// If decay multiplier drops below this, observation becomes stale.
|
||||
/// Default: 0.50
|
||||
/// </summary>
|
||||
[JsonPropertyName("staleness_threshold")]
|
||||
public required double StalenessThreshold { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the current decay multiplier.
|
||||
/// </summary>
|
||||
public double CalculateDecay(DateTimeOffset now)
|
||||
{
|
||||
var ageDays = (now - RefreshedAt).TotalDays;
|
||||
if (ageDays <= 0)
|
||||
return 1.0;
|
||||
|
||||
var decay = Math.Exp(-Math.Log(2) * ageDays / HalfLifeDays);
|
||||
return Math.Max(Floor, decay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the observation is stale (decay below threshold).
|
||||
/// </summary>
|
||||
public bool IsStale(DateTimeOffset now) =>
|
||||
CalculateDecay(now) < StalenessThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// Creates ObservationDecay with default settings.
|
||||
/// </summary>
|
||||
public static ObservationDecay Create(DateTimeOffset observedAt, DateTimeOffset? refreshedAt = null) => new()
|
||||
{
|
||||
ObservedAt = observedAt,
|
||||
RefreshedAt = refreshedAt ?? observedAt,
|
||||
HalfLifeDays = 14.0,
|
||||
Floor = 0.35,
|
||||
StalenessThreshold = 0.50
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fresh observation (just recorded).
|
||||
/// </summary>
|
||||
public static ObservationDecay Fresh(DateTimeOffset now) =>
|
||||
Create(now, now);
|
||||
|
||||
/// <summary>
|
||||
/// Creates ObservationDecay with custom settings.
|
||||
/// </summary>
|
||||
public static ObservationDecay WithSettings(
|
||||
DateTimeOffset observedAt,
|
||||
DateTimeOffset refreshedAt,
|
||||
double halfLifeDays,
|
||||
double floor,
|
||||
double stalenessThreshold) => new()
|
||||
{
|
||||
ObservedAt = observedAt,
|
||||
RefreshedAt = refreshedAt,
|
||||
HalfLifeDays = halfLifeDays,
|
||||
Floor = floor,
|
||||
StalenessThreshold = stalenessThreshold
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Observation state for CVE tracking, independent of VEX status.
|
||||
/// Allows a CVE to be "Affected" (VEX) but "PendingDeterminization" (observation).
|
||||
/// </summary>
|
||||
public enum ObservationState
|
||||
{
|
||||
/// <summary>
|
||||
/// Initial state: CVE discovered but evidence incomplete.
|
||||
/// Triggers guardrail-based policy evaluation.
|
||||
/// </summary>
|
||||
PendingDeterminization = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Evidence sufficient for confident determination.
|
||||
/// Normal policy evaluation applies.
|
||||
/// </summary>
|
||||
Determined = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Multiple signals conflict (K4 Conflict state).
|
||||
/// Requires human review regardless of confidence.
|
||||
/// </summary>
|
||||
Disputed = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Evidence decayed below threshold; needs refresh.
|
||||
/// Auto-triggered when decay > threshold.
|
||||
/// </summary>
|
||||
StaleRequiresRefresh = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Manually flagged for review.
|
||||
/// Bypasses automatic determinization.
|
||||
/// </summary>
|
||||
ManualReviewRequired = 4,
|
||||
|
||||
/// <summary>
|
||||
/// CVE suppressed/ignored by policy exception.
|
||||
/// Evidence tracking continues but decisions skip.
|
||||
/// </summary>
|
||||
Suppressed = 5
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a missing signal that contributes to uncertainty.
|
||||
/// </summary>
|
||||
public sealed record SignalGap
|
||||
{
|
||||
/// <summary>
|
||||
/// Signal name (e.g., "epss", "vex", "reachability").
|
||||
/// </summary>
|
||||
[JsonPropertyName("signal")]
|
||||
public required string Signal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason the signal is missing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reason")]
|
||||
public required SignalGapReason Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Prior assumption used in absence of signal.
|
||||
/// </summary>
|
||||
[JsonPropertyName("prior")]
|
||||
public double? Prior { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Weight this signal contributes to total uncertainty.
|
||||
/// </summary>
|
||||
[JsonPropertyName("weight")]
|
||||
public double Weight { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reason a signal is missing.
|
||||
/// </summary>
|
||||
public enum SignalGapReason
|
||||
{
|
||||
/// <summary>Signal not yet queried.</summary>
|
||||
NotQueried,
|
||||
|
||||
/// <summary>Signal legitimately does not exist (e.g., EPSS not published yet).</summary>
|
||||
NotAvailable,
|
||||
|
||||
/// <summary>Signal query failed due to external error.</summary>
|
||||
QueryFailed,
|
||||
|
||||
/// <summary>Signal not applicable for this artifact type.</summary>
|
||||
NotApplicable
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Query status for a signal.
|
||||
/// Distinguishes between "not yet queried", "queried with result", and "query failed".
|
||||
/// </summary>
|
||||
public enum SignalQueryStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Signal has not been queried yet.
|
||||
/// Default state before any lookup attempt.
|
||||
/// </summary>
|
||||
NotQueried = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Signal query succeeded.
|
||||
/// Value may be present or null (signal legitimately absent).
|
||||
/// </summary>
|
||||
Queried = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Signal query failed due to error (network, API timeout, etc.).
|
||||
/// Value is null but reason is external failure, not absence.
|
||||
/// </summary>
|
||||
Failed = 2
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Policy.Determinization.Evidence;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Point-in-time snapshot of all signals for a CVE observation.
|
||||
/// Used as input to uncertainty scoring.
|
||||
/// </summary>
|
||||
public sealed record SignalSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// CVE identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cve")]
|
||||
public required string Cve { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Component PURL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("purl")]
|
||||
public required string Purl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// EPSS signal.
|
||||
/// </summary>
|
||||
[JsonPropertyName("epss")]
|
||||
public required SignalState<EpssEvidence> Epss { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// VEX signal.
|
||||
/// </summary>
|
||||
[JsonPropertyName("vex")]
|
||||
public required SignalState<VexClaimSummary> Vex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reachability signal.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reachability")]
|
||||
public required SignalState<ReachabilityEvidence> Reachability { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime signal.
|
||||
/// </summary>
|
||||
[JsonPropertyName("runtime")]
|
||||
public required SignalState<RuntimeEvidence> Runtime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Backport signal.
|
||||
/// </summary>
|
||||
[JsonPropertyName("backport")]
|
||||
public required SignalState<BackportEvidence> Backport { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SBOM lineage signal.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sbom")]
|
||||
public required SignalState<SbomLineageEvidence> Sbom { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVSS signal.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cvss")]
|
||||
public required SignalState<CvssEvidence> Cvss { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When this snapshot was captured (UTC).
|
||||
/// </summary>
|
||||
[JsonPropertyName("snapshot_at")]
|
||||
public required DateTimeOffset SnapshotAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty snapshot with all signals NotQueried.
|
||||
/// </summary>
|
||||
public static SignalSnapshot Empty(string cve, string purl, DateTimeOffset snapshotAt) => new()
|
||||
{
|
||||
Cve = cve,
|
||||
Purl = purl,
|
||||
Epss = SignalState<EpssEvidence>.NotQueried(),
|
||||
Vex = SignalState<VexClaimSummary>.NotQueried(),
|
||||
Reachability = SignalState<ReachabilityEvidence>.NotQueried(),
|
||||
Runtime = SignalState<RuntimeEvidence>.NotQueried(),
|
||||
Backport = SignalState<BackportEvidence>.NotQueried(),
|
||||
Sbom = SignalState<SbomLineageEvidence>.NotQueried(),
|
||||
Cvss = SignalState<CvssEvidence>.NotQueried(),
|
||||
SnapshotAt = snapshotAt
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a signal value with query status metadata.
|
||||
/// Distinguishes between: not queried, queried with value, queried but absent, query failed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The signal value type.</typeparam>
|
||||
public sealed record SignalState<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Query status for this signal.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public required SignalQueryStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signal value, if queried and present.
|
||||
/// Null can mean: not queried, legitimately absent, or query failed.
|
||||
/// Check Status to disambiguate.
|
||||
/// </summary>
|
||||
[JsonPropertyName("value")]
|
||||
public T? Value { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When this signal was last queried (UTC).
|
||||
/// Null if never queried.
|
||||
/// </summary>
|
||||
[JsonPropertyName("queried_at")]
|
||||
public DateTimeOffset? QueriedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if Status == Failed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("error")]
|
||||
public string? Error { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a SignalState in NotQueried status.
|
||||
/// </summary>
|
||||
public static SignalState<T> NotQueried() => new()
|
||||
{
|
||||
Status = SignalQueryStatus.NotQueried,
|
||||
Value = default,
|
||||
QueriedAt = null,
|
||||
Error = null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a SignalState with a successful query result.
|
||||
/// Value may be null if the signal legitimately does not exist.
|
||||
/// </summary>
|
||||
public static SignalState<T> Queried(T? value, DateTimeOffset queriedAt) => new()
|
||||
{
|
||||
Status = SignalQueryStatus.Queried,
|
||||
Value = value,
|
||||
QueriedAt = queriedAt,
|
||||
Error = null
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a SignalState representing a failed query.
|
||||
/// </summary>
|
||||
public static SignalState<T> Failed(string error, DateTimeOffset attemptedAt) => new()
|
||||
{
|
||||
Status = SignalQueryStatus.Failed,
|
||||
Value = default,
|
||||
QueriedAt = attemptedAt,
|
||||
Error = error
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the signal was queried and has a non-null value.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool HasValue => Status == SignalQueryStatus.Queried && Value is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the signal query failed.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsFailed => Status == SignalQueryStatus.Failed;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the signal has not been queried yet.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsNotQueried => Status == SignalQueryStatus.NotQueried;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Uncertainty tier classification based on entropy score.
|
||||
/// </summary>
|
||||
public enum UncertaintyTier
|
||||
{
|
||||
/// <summary>
|
||||
/// Very high confidence (entropy < 0.2).
|
||||
/// All or most key signals present and consistent.
|
||||
/// </summary>
|
||||
Minimal = 0,
|
||||
|
||||
/// <summary>
|
||||
/// High confidence (entropy 0.2-0.4).
|
||||
/// Most key signals present.
|
||||
/// </summary>
|
||||
Low = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Moderate confidence (entropy 0.4-0.6).
|
||||
/// Some signals missing or conflicting.
|
||||
/// </summary>
|
||||
Moderate = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Low confidence (entropy 0.6-0.8).
|
||||
/// Many signals missing or conflicting.
|
||||
/// </summary>
|
||||
High = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Very low confidence (entropy >= 0.8).
|
||||
/// Critical signals missing or heavily conflicting.
|
||||
/// </summary>
|
||||
Critical = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quantifies knowledge completeness (not code entropy).
|
||||
/// Calculated from signal presence/absence weighted by importance.
|
||||
/// Formula: entropy = 1 - (sum of weighted present signals / max possible weight)
|
||||
/// </summary>
|
||||
public sealed record UncertaintyScore
|
||||
{
|
||||
/// <summary>
|
||||
/// Entropy value [0.0, 1.0].
|
||||
/// 0 = complete knowledge, 1 = complete uncertainty.
|
||||
/// </summary>
|
||||
[JsonPropertyName("entropy")]
|
||||
public required double Entropy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Uncertainty tier derived from entropy.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tier")]
|
||||
public required UncertaintyTier Tier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Missing signals contributing to uncertainty.
|
||||
/// </summary>
|
||||
[JsonPropertyName("gaps")]
|
||||
public required IReadOnlyList<SignalGap> Gaps { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total weight of present signals.
|
||||
/// </summary>
|
||||
[JsonPropertyName("present_weight")]
|
||||
public required double PresentWeight { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum possible weight (sum of all signal weights).
|
||||
/// </summary>
|
||||
[JsonPropertyName("max_weight")]
|
||||
public required double MaxWeight { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When this score was calculated (UTC).
|
||||
/// </summary>
|
||||
[JsonPropertyName("calculated_at")]
|
||||
public required DateTimeOffset CalculatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an UncertaintyScore with calculated tier.
|
||||
/// </summary>
|
||||
public static UncertaintyScore Create(
|
||||
double entropy,
|
||||
IReadOnlyList<SignalGap> gaps,
|
||||
double presentWeight,
|
||||
double maxWeight,
|
||||
DateTimeOffset calculatedAt)
|
||||
{
|
||||
if (entropy < 0.0 || entropy > 1.0)
|
||||
throw new ArgumentOutOfRangeException(nameof(entropy), "Entropy must be in [0.0, 1.0]");
|
||||
|
||||
var tier = entropy switch
|
||||
{
|
||||
< 0.2 => UncertaintyTier.Minimal,
|
||||
< 0.4 => UncertaintyTier.Low,
|
||||
< 0.6 => UncertaintyTier.Moderate,
|
||||
< 0.8 => UncertaintyTier.High,
|
||||
_ => UncertaintyTier.Critical
|
||||
};
|
||||
|
||||
return new UncertaintyScore
|
||||
{
|
||||
Entropy = entropy,
|
||||
Tier = tier,
|
||||
Gaps = gaps,
|
||||
PresentWeight = presentWeight,
|
||||
MaxWeight = maxWeight,
|
||||
CalculatedAt = calculatedAt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a zero-entropy score (complete knowledge).
|
||||
/// </summary>
|
||||
public static UncertaintyScore Zero(double maxWeight, DateTimeOffset calculatedAt) =>
|
||||
Create(0.0, Array.Empty<SignalGap>(), maxWeight, maxWeight, calculatedAt);
|
||||
}
|
||||
Reference in New Issue
Block a user