From ae78af4692d20f78e3890fc89c9792ce9bee66b8 Mon Sep 17 00:00:00 2001 From: StellaOps Bot Date: Sun, 4 Jan 2026 13:25:15 +0200 Subject: [PATCH] DET-004: Refactor Policy Replay and Deltas for determinism - ReplayEngine: inject TimeProvider - ReplayReport: inject TimeProvider and IGuidProvider via builder - ReplayResult: add TimeProvider parameter to Failed() method - DeltaComputer: inject TimeProvider - DeltaVerdictBuilder: inject TimeProvider Replace DateTimeOffset.UtcNow and Guid.NewGuid() with injected providers Sprint: SPRINT_20260104_001_BE_determinism_timeprovider_injection --- .../StellaOps.Policy/Deltas/DeltaComputer.cs | 7 +++++-- .../StellaOps.Policy/Deltas/DeltaVerdict.cs | 9 ++++++--- .../StellaOps.Policy/Replay/ReplayEngine.cs | 5 ++++- .../StellaOps.Policy/Replay/ReplayReport.cs | 16 +++++++++++++--- .../StellaOps.Policy/Replay/ReplayResult.cs | 4 ++-- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/Policy/__Libraries/StellaOps.Policy/Deltas/DeltaComputer.cs b/src/Policy/__Libraries/StellaOps.Policy/Deltas/DeltaComputer.cs index c2f66bafe..296fd82af 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Deltas/DeltaComputer.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Deltas/DeltaComputer.cs @@ -16,13 +16,16 @@ public sealed class DeltaComputer : IDeltaComputer { private readonly ISnapshotService _snapshotService; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public DeltaComputer( ISnapshotService snapshotService, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _snapshotService = snapshotService; _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -58,7 +61,7 @@ public sealed class DeltaComputer : IDeltaComputer var delta = new SecurityStateDelta { DeltaId = "", // Computed below - ComputedAt = DateTimeOffset.UtcNow, + ComputedAt = _timeProvider.GetUtcNow(), BaselineSnapshotId = baselineSnapshotId, TargetSnapshotId = targetSnapshotId, Artifact = artifact, diff --git a/src/Policy/__Libraries/StellaOps.Policy/Deltas/DeltaVerdict.cs b/src/Policy/__Libraries/StellaOps.Policy/Deltas/DeltaVerdict.cs index fa2f8925e..5d31fbbe2 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Deltas/DeltaVerdict.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Deltas/DeltaVerdict.cs @@ -127,6 +127,7 @@ public sealed class DeltaVerdictBuilder private static readonly IVerdictIdGenerator DefaultIdGenerator = new VerdictIdGenerator(); private readonly IVerdictIdGenerator _idGenerator; + private readonly TimeProvider _timeProvider; private DeltaVerdictStatus _status = DeltaVerdictStatus.Pass; private DeltaGateLevel _gate = DeltaGateLevel.G1; private int _riskPoints; @@ -139,7 +140,7 @@ public sealed class DeltaVerdictBuilder /// /// Creates a new with the default ID generator. /// - public DeltaVerdictBuilder() : this(DefaultIdGenerator) + public DeltaVerdictBuilder() : this(DefaultIdGenerator, TimeProvider.System) { } @@ -147,9 +148,11 @@ public sealed class DeltaVerdictBuilder /// Creates a new with a custom ID generator. /// /// Custom verdict ID generator for testing or specialized scenarios. - public DeltaVerdictBuilder(IVerdictIdGenerator idGenerator) + /// Time provider for deterministic timestamps. + public DeltaVerdictBuilder(IVerdictIdGenerator idGenerator, TimeProvider? timeProvider = null) { _idGenerator = idGenerator ?? throw new ArgumentNullException(nameof(idGenerator)); + _timeProvider = timeProvider ?? TimeProvider.System; } public DeltaVerdictBuilder WithStatus(DeltaVerdictStatus status) @@ -241,7 +244,7 @@ public sealed class DeltaVerdictBuilder { VerdictId = verdictId, DeltaId = deltaId, - EvaluatedAt = DateTimeOffset.UtcNow, + EvaluatedAt = _timeProvider.GetUtcNow(), Status = _status, RecommendedGate = _gate, RiskPoints = _riskPoints, diff --git a/src/Policy/__Libraries/StellaOps.Policy/Replay/ReplayEngine.cs b/src/Policy/__Libraries/StellaOps.Policy/Replay/ReplayEngine.cs index 92b28fde1..cabec3584 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Replay/ReplayEngine.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Replay/ReplayEngine.cs @@ -14,16 +14,19 @@ public sealed class ReplayEngine : IReplayEngine private readonly IKnowledgeSourceResolver _sourceResolver; private readonly IVerdictComparer _verdictComparer; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public ReplayEngine( ISnapshotService snapshotService, IKnowledgeSourceResolver sourceResolver, IVerdictComparer verdictComparer, + TimeProvider? timeProvider = null, ILogger? logger = null) { _snapshotService = snapshotService ?? throw new ArgumentNullException(nameof(snapshotService)); _sourceResolver = sourceResolver ?? throw new ArgumentNullException(nameof(sourceResolver)); _verdictComparer = verdictComparer ?? throw new ArgumentNullException(nameof(verdictComparer)); + _timeProvider = timeProvider ?? TimeProvider.System; _logger = logger ?? NullLogger.Instance; } @@ -89,7 +92,7 @@ public sealed class ReplayEngine : IReplayEngine OriginalVerdict = originalVerdict, DeltaReport = deltaReport, SnapshotId = request.SnapshotId, - ReplayedAt = DateTimeOffset.UtcNow, + ReplayedAt = _timeProvider.GetUtcNow(), Duration = stopwatch.Elapsed }; } diff --git a/src/Policy/__Libraries/StellaOps.Policy/Replay/ReplayReport.cs b/src/Policy/__Libraries/StellaOps.Policy/Replay/ReplayReport.cs index 41e1002a5..dfa669ecb 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Replay/ReplayReport.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Replay/ReplayReport.cs @@ -1,3 +1,5 @@ +using StellaOps.Determinism; + namespace StellaOps.Policy.Replay; /// @@ -111,11 +113,19 @@ public sealed class ReplayReportBuilder private readonly ReplayResult _result; private readonly ReplayRequest _request; private readonly List _recommendations = []; + private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; - public ReplayReportBuilder(ReplayRequest request, ReplayResult result) + public ReplayReportBuilder( + ReplayRequest request, + ReplayResult result, + TimeProvider? timeProvider = null, + IGuidProvider? guidProvider = null) { _request = request ?? throw new ArgumentNullException(nameof(request)); _result = result ?? throw new ArgumentNullException(nameof(result)); + _timeProvider = timeProvider ?? TimeProvider.System; + _guidProvider = guidProvider ?? SystemGuidProvider.Instance; } public ReplayReportBuilder AddRecommendation(string recommendation) @@ -150,8 +160,8 @@ public sealed class ReplayReportBuilder { return new ReplayReport { - ReportId = $"rpt:{Guid.NewGuid():N}", - GeneratedAt = DateTimeOffset.UtcNow, + ReportId = $"rpt:{_guidProvider.NewGuid():N}", + GeneratedAt = _timeProvider.GetUtcNow(), ArtifactDigest = _request.ArtifactDigest, SnapshotId = _request.SnapshotId, OriginalVerdictId = _request.OriginalVerdictId, diff --git a/src/Policy/__Libraries/StellaOps.Policy/Replay/ReplayResult.cs b/src/Policy/__Libraries/StellaOps.Policy/Replay/ReplayResult.cs index 66de08246..63afca9e7 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Replay/ReplayResult.cs +++ b/src/Policy/__Libraries/StellaOps.Policy/Replay/ReplayResult.cs @@ -43,12 +43,12 @@ public sealed record ReplayResult /// /// Creates a failed result. /// - public static ReplayResult Failed(string snapshotId, string error) => new() + public static ReplayResult Failed(string snapshotId, string error, TimeProvider? timeProvider = null) => new() { MatchStatus = ReplayMatchStatus.ReplayFailed, ReplayedVerdict = ReplayedVerdict.Empty, SnapshotId = snapshotId, - ReplayedAt = DateTimeOffset.UtcNow, + ReplayedAt = (timeProvider ?? TimeProvider.System).GetUtcNow(), DeltaReport = new ReplayDeltaReport { Summary = error,