Files
git.stella-ops.org/src/Policy/__Libraries/StellaOps.Policy/Deltas/DeltaVerdict.cs
StellaOps Bot b9f71fc7e9 sprints work
2025-12-24 21:46:08 +02:00

268 lines
7.5 KiB
C#

namespace StellaOps.Policy.Deltas;
/// <summary>
/// Verdict for a security state delta.
/// Determines whether a change should be allowed to proceed.
/// </summary>
public sealed record DeltaVerdict
{
/// <summary>
/// Unique identifier for this verdict.
/// </summary>
public required string VerdictId { get; init; }
/// <summary>
/// Reference to the delta being evaluated.
/// </summary>
public required string DeltaId { get; init; }
/// <summary>
/// When this verdict was rendered.
/// </summary>
public required DateTimeOffset EvaluatedAt { get; init; }
/// <summary>
/// The verdict outcome.
/// </summary>
public required DeltaVerdictStatus Status { get; init; }
/// <summary>
/// Recommended gate level based on delta risk.
/// </summary>
public DeltaGateLevel RecommendedGate { get; init; }
/// <summary>
/// Risk points consumed by this change.
/// </summary>
public int RiskPoints { get; init; }
/// <summary>
/// Drivers that contributed to the verdict.
/// </summary>
public IReadOnlyList<DeltaDriver> BlockingDrivers { get; init; } = [];
/// <summary>
/// Drivers that raised warnings but didn't block.
/// </summary>
public IReadOnlyList<DeltaDriver> WarningDrivers { get; init; } = [];
/// <summary>
/// Applied exceptions that allowed blocking drivers.
/// </summary>
public IReadOnlyList<string> AppliedExceptions { get; init; } = [];
/// <summary>
/// Human-readable explanation.
/// </summary>
public string? Explanation { get; init; }
/// <summary>
/// Recommendations for addressing issues.
/// </summary>
public IReadOnlyList<string> Recommendations { get; init; } = [];
}
/// <summary>
/// Possible verdict outcomes for a delta.
/// </summary>
public enum DeltaVerdictStatus
{
/// <summary>
/// Delta is safe to proceed.
/// </summary>
Pass,
/// <summary>
/// Delta has warnings but can proceed.
/// </summary>
Warn,
/// <summary>
/// Delta should not proceed without remediation.
/// </summary>
Fail,
/// <summary>
/// Delta is blocked but covered by exceptions.
/// </summary>
PassWithExceptions
}
/// <summary>
/// Gate levels aligned with diff-aware release gates.
/// </summary>
public enum DeltaGateLevel
{
/// <summary>
/// G0: No-risk (docs, comments only).
/// </summary>
G0,
/// <summary>
/// G1: Low risk (unit tests, 1 review).
/// </summary>
G1,
/// <summary>
/// G2: Moderate risk (integration tests, code owner, canary).
/// </summary>
G2,
/// <summary>
/// G3: High risk (security scan, migration plan, release captain).
/// </summary>
G3,
/// <summary>
/// G4: Very high risk (formal review, extended canary, comms plan).
/// </summary>
G4
}
/// <summary>
/// Builder for delta verdicts.
/// </summary>
public sealed class DeltaVerdictBuilder
{
private static readonly IVerdictIdGenerator DefaultIdGenerator = new VerdictIdGenerator();
private readonly IVerdictIdGenerator _idGenerator;
private DeltaVerdictStatus _status = DeltaVerdictStatus.Pass;
private DeltaGateLevel _gate = DeltaGateLevel.G1;
private int _riskPoints;
private readonly List<DeltaDriver> _blockingDrivers = [];
private readonly List<DeltaDriver> _warningDrivers = [];
private readonly List<string> _exceptions = [];
private readonly List<string> _recommendations = [];
private string? _explanation;
/// <summary>
/// Creates a new <see cref="DeltaVerdictBuilder"/> with the default ID generator.
/// </summary>
public DeltaVerdictBuilder() : this(DefaultIdGenerator)
{
}
/// <summary>
/// Creates a new <see cref="DeltaVerdictBuilder"/> with a custom ID generator.
/// </summary>
/// <param name="idGenerator">Custom verdict ID generator for testing or specialized scenarios.</param>
public DeltaVerdictBuilder(IVerdictIdGenerator idGenerator)
{
_idGenerator = idGenerator ?? throw new ArgumentNullException(nameof(idGenerator));
}
public DeltaVerdictBuilder WithStatus(DeltaVerdictStatus status)
{
_status = status;
return this;
}
public DeltaVerdictBuilder WithGate(DeltaGateLevel gate)
{
_gate = gate;
return this;
}
public DeltaVerdictBuilder WithRiskPoints(int points)
{
_riskPoints = points;
return this;
}
public DeltaVerdictBuilder AddBlockingDriver(DeltaDriver driver)
{
_blockingDrivers.Add(driver);
_status = DeltaVerdictStatus.Fail;
// Escalate gate based on severity
if (driver.Severity == DeltaDriverSeverity.Critical && _gate < DeltaGateLevel.G4)
_gate = DeltaGateLevel.G4;
else if (driver.Severity == DeltaDriverSeverity.High && _gate < DeltaGateLevel.G3)
_gate = DeltaGateLevel.G3;
return this;
}
public DeltaVerdictBuilder AddWarningDriver(DeltaDriver driver)
{
_warningDrivers.Add(driver);
if (_status == DeltaVerdictStatus.Pass)
_status = DeltaVerdictStatus.Warn;
// Escalate gate for medium severity warnings
if (driver.Severity >= DeltaDriverSeverity.Medium && _gate < DeltaGateLevel.G2)
_gate = DeltaGateLevel.G2;
return this;
}
public DeltaVerdictBuilder AddException(string exceptionId)
{
_exceptions.Add(exceptionId);
return this;
}
public DeltaVerdictBuilder AddRecommendation(string recommendation)
{
_recommendations.Add(recommendation);
return this;
}
public DeltaVerdictBuilder WithExplanation(string explanation)
{
_explanation = explanation;
return this;
}
public DeltaVerdict Build(string deltaId)
{
// If all blocking drivers are excepted, change to PassWithExceptions
if (_status == DeltaVerdictStatus.Fail &&
_blockingDrivers.Count > 0 &&
_exceptions.Count >= _blockingDrivers.Count)
{
_status = DeltaVerdictStatus.PassWithExceptions;
}
var blockingDrivers = _blockingDrivers.ToList();
var warningDrivers = _warningDrivers.ToList();
var appliedExceptions = _exceptions.ToList();
// Compute content-addressed VerdictId from inputs
var verdictId = _idGenerator.ComputeVerdictId(
deltaId,
blockingDrivers,
warningDrivers,
appliedExceptions,
_gate);
return new DeltaVerdict
{
VerdictId = verdictId,
DeltaId = deltaId,
EvaluatedAt = DateTimeOffset.UtcNow,
Status = _status,
RecommendedGate = _gate,
RiskPoints = _riskPoints,
BlockingDrivers = blockingDrivers,
WarningDrivers = warningDrivers,
AppliedExceptions = appliedExceptions,
Explanation = _explanation ?? GenerateExplanation(),
Recommendations = _recommendations.ToList()
};
}
private string GenerateExplanation()
{
return _status switch
{
DeltaVerdictStatus.Pass => "No blocking changes detected",
DeltaVerdictStatus.Warn => $"{_warningDrivers.Count} warning(s) detected",
DeltaVerdictStatus.Fail => $"{_blockingDrivers.Count} blocking issue(s) detected",
DeltaVerdictStatus.PassWithExceptions => $"Blocked by {_blockingDrivers.Count} issue(s), covered by exceptions",
_ => "Unknown status"
};
}
}