up
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
namespace StellaOps.Policy.Scoring;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates freshness multiplier for evidence based on age.
|
||||
/// Uses basis-point math for determinism (no floating point).
|
||||
/// </summary>
|
||||
public sealed class EvidenceFreshnessCalculator
|
||||
{
|
||||
private readonly FreshnessMultiplierConfig _config;
|
||||
|
||||
public EvidenceFreshnessCalculator(FreshnessMultiplierConfig? config = null)
|
||||
{
|
||||
_config = config ?? FreshnessMultiplierConfig.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the freshness multiplier for evidence collected at a given timestamp.
|
||||
/// </summary>
|
||||
/// <param name="evidenceTimestamp">When the evidence was collected.</param>
|
||||
/// <param name="asOf">Reference time for freshness calculation (explicit, no implicit time).</param>
|
||||
/// <returns>Multiplier in basis points (10000 = 100%).</returns>
|
||||
public int CalculateMultiplierBps(DateTimeOffset evidenceTimestamp, DateTimeOffset asOf)
|
||||
{
|
||||
if (evidenceTimestamp > asOf)
|
||||
{
|
||||
return _config.Buckets[0].MultiplierBps; // Future evidence gets max freshness
|
||||
}
|
||||
|
||||
var ageDays = (int)(asOf - evidenceTimestamp).TotalDays;
|
||||
|
||||
foreach (var bucket in _config.Buckets)
|
||||
{
|
||||
if (ageDays <= bucket.MaxAgeDays)
|
||||
{
|
||||
return bucket.MultiplierBps;
|
||||
}
|
||||
}
|
||||
|
||||
return _config.Buckets[^1].MultiplierBps; // Fallback to oldest bucket
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies freshness multiplier to a base score.
|
||||
/// </summary>
|
||||
/// <param name="baseScore">Score in range 0-100.</param>
|
||||
/// <param name="evidenceTimestamp">When the evidence was collected.</param>
|
||||
/// <param name="asOf">Reference time for freshness calculation.</param>
|
||||
/// <returns>Adjusted score (integer, no floating point).</returns>
|
||||
public int ApplyFreshness(int baseScore, DateTimeOffset evidenceTimestamp, DateTimeOffset asOf)
|
||||
{
|
||||
var multiplierBps = CalculateMultiplierBps(evidenceTimestamp, asOf);
|
||||
return (baseScore * multiplierBps) / 10000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace StellaOps.Policy.Scoring;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a freshness bucket for evidence age-based scoring decay.
|
||||
/// </summary>
|
||||
/// <param name="MaxAgeDays">Maximum age in days for this bucket (inclusive upper bound).</param>
|
||||
/// <param name="MultiplierBps">Multiplier in basis points (10000 = 100%).</param>
|
||||
public sealed record FreshnessBucket(int MaxAgeDays, int MultiplierBps);
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for evidence freshness multipliers.
|
||||
/// Default buckets per determinism advisory: 7d=10000, 30d=9000, 90d=7500, 180d=6000, 365d=4000, >365d=2000.
|
||||
/// </summary>
|
||||
public sealed record FreshnessMultiplierConfig
|
||||
{
|
||||
public required IReadOnlyList<FreshnessBucket> Buckets { get; init; }
|
||||
|
||||
public static FreshnessMultiplierConfig Default { get; } = new()
|
||||
{
|
||||
Buckets =
|
||||
[
|
||||
new FreshnessBucket(7, 10000),
|
||||
new FreshnessBucket(30, 9000),
|
||||
new FreshnessBucket(90, 7500),
|
||||
new FreshnessBucket(180, 6000),
|
||||
new FreshnessBucket(365, 4000),
|
||||
new FreshnessBucket(int.MaxValue, 2000)
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Policy.Scoring;
|
||||
|
||||
/// <summary>
|
||||
/// Structured explanation of a factor's contribution to the final score.
|
||||
/// </summary>
|
||||
/// <param name="Factor">Factor identifier (e.g., "reachability", "evidence", "provenance").</param>
|
||||
/// <param name="Value">Computed value for this factor (0-100 range).</param>
|
||||
/// <param name="Reason">Human-readable explanation of how the value was computed.</param>
|
||||
/// <param name="ContributingDigests">Optional digests of objects that contributed to this factor.</param>
|
||||
public sealed record ScoreExplanation(
|
||||
string Factor,
|
||||
int Value,
|
||||
string Reason,
|
||||
IReadOnlyList<string>? ContributingDigests = null);
|
||||
|
||||
/// <summary>
|
||||
/// Builder for accumulating score explanations during scoring pipeline.
|
||||
/// </summary>
|
||||
public sealed class ScoreExplainBuilder
|
||||
{
|
||||
private readonly List<ScoreExplanation> _explanations = [];
|
||||
|
||||
public ScoreExplainBuilder Add(string factor, int value, string reason, IReadOnlyList<string>? digests = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(factor);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(reason);
|
||||
|
||||
IReadOnlyList<string>? normalizedDigests = null;
|
||||
if (digests is { Count: > 0 })
|
||||
{
|
||||
normalizedDigests = digests
|
||||
.Where(d => !string.IsNullOrWhiteSpace(d))
|
||||
.Select(d => d.Trim())
|
||||
.OrderBy(d => d, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
_explanations.Add(new ScoreExplanation(factor.Trim(), value, reason, normalizedDigests));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ScoreExplainBuilder AddReachability(int hops, int score, string entrypoint)
|
||||
{
|
||||
var reason = hops switch
|
||||
{
|
||||
0 => $"Direct entry point: {entrypoint}",
|
||||
<= 2 => $"{hops} hops from {entrypoint}",
|
||||
_ => $"{hops} hops from nearest entry point"
|
||||
};
|
||||
|
||||
return Add("reachability", score, reason);
|
||||
}
|
||||
|
||||
public ScoreExplainBuilder AddEvidence(int points, int freshnessMultiplierBps, int ageDays)
|
||||
{
|
||||
var freshnessPercent = freshnessMultiplierBps / 100;
|
||||
var reason = $"{points} evidence points, {ageDays} days old ({freshnessPercent}% freshness)";
|
||||
return Add("evidence", (points * freshnessMultiplierBps) / 10000, reason);
|
||||
}
|
||||
|
||||
public ScoreExplainBuilder AddProvenance(string level, int score)
|
||||
{
|
||||
return Add("provenance", score, $"Provenance level: {level}");
|
||||
}
|
||||
|
||||
public ScoreExplainBuilder AddBaseSeverity(decimal cvss, int score)
|
||||
{
|
||||
return Add("baseSeverity", score, $"CVSS {cvss:F1} mapped to {score}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the explanation list, sorted by factor name for determinism.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ScoreExplanation> Build()
|
||||
{
|
||||
return _explanations
|
||||
.OrderBy(e => e.Factor, StringComparer.Ordinal)
|
||||
.ThenBy(e => e.ContributingDigests?.FirstOrDefault() ?? "", StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ public static class SplSchemaResource
|
||||
private const string SchemaResourceName = "StellaOps.Policy.Schemas.spl-schema@1.json";
|
||||
private const string SampleResourceName = "StellaOps.Policy.Schemas.spl-sample@1.json";
|
||||
|
||||
public static string GetSchema() => ReadSchemaJson();
|
||||
|
||||
public static string GetSample() => ReadSampleJson();
|
||||
|
||||
public static Stream OpenSchemaStream()
|
||||
{
|
||||
return OpenResourceStream(SchemaResourceName);
|
||||
|
||||
10
src/Policy/__Libraries/StellaOps.Policy/TASKS.md
Normal file
10
src/Policy/__Libraries/StellaOps.Policy/TASKS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Policy Library Local Tasks
|
||||
|
||||
This file mirrors sprint work for the `StellaOps.Policy` library.
|
||||
|
||||
| Task ID | Sprint | Status | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `DET-3401-001` | `docs/implplan/SPRINT_3401_0001_0001_determinism_scoring_foundations.md` | DONE (2025-12-14) | Added `FreshnessBucket` + `FreshnessMultiplierConfig` in `src/Policy/__Libraries/StellaOps.Policy/Scoring/FreshnessModels.cs` and covered bucket boundaries in `src/Policy/__Tests/StellaOps.Policy.Tests/Scoring/EvidenceFreshnessCalculatorTests.cs`. |
|
||||
| `DET-3401-002` | `docs/implplan/SPRINT_3401_0001_0001_determinism_scoring_foundations.md` | DONE (2025-12-14) | Implemented `EvidenceFreshnessCalculator` in `src/Policy/__Libraries/StellaOps.Policy/Scoring/EvidenceFreshnessCalculator.cs`. |
|
||||
| `DET-3401-009` | `docs/implplan/SPRINT_3401_0001_0001_determinism_scoring_foundations.md` | DONE (2025-12-14) | Added `ScoreExplanation` + `ScoreExplainBuilder` in `src/Policy/__Libraries/StellaOps.Policy/Scoring/ScoreExplanation.cs` and tests in `src/Policy/__Tests/StellaOps.Policy.Tests/Scoring/ScoreExplainBuilderTests.cs`. |
|
||||
|
||||
Reference in New Issue
Block a user