diff --git a/docs/implplan/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md b/docs/implplan/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md index 801a95988..d6624498d 100644 --- a/docs/implplan/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md +++ b/docs/implplan/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md @@ -54,10 +54,10 @@ | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| 1 | DET-001 | TODO | Audit complete | Guild | Full audit: count all DateTimeOffset.UtcNow/DateTime.UtcNow/Guid.NewGuid/Random.Shared by project | -| 2 | DET-002 | TODO | DET-001 | Guild | Ensure IGuidProvider abstraction exists in StellaOps.Determinism.Abstractions | -| 3 | DET-003 | TODO | DET-001 | Guild | Ensure TimeProvider registration pattern documented | -| 4 | DET-004 | TODO | DET-002, DET-003 | Guild | Refactor Policy module (Policy.Unknowns, PolicyDsl, etc.) | +| 1 | DET-001 | DONE | Audit complete | Guild | Full audit: count all DateTimeOffset.UtcNow/DateTime.UtcNow/Guid.NewGuid/Random.Shared by project | +| 2 | DET-002 | DONE | DET-001 | Guild | Ensure IGuidProvider abstraction exists in StellaOps.Determinism.Abstractions | +| 3 | DET-003 | DONE | DET-001 | Guild | Ensure TimeProvider registration pattern documented | +| 4 | DET-004 | DOING | DET-002, DET-003 | Guild | Refactor Policy module (Policy.Unknowns, PolicyDsl, etc.) | | 5 | DET-005 | TODO | DET-002, DET-003 | Guild | Refactor Provcache module | | 6 | DET-006 | TODO | DET-002, DET-003 | Guild | Refactor Provenance module | | 7 | DET-007 | TODO | DET-002, DET-003 | Guild | Refactor ReachGraph module | @@ -110,6 +110,11 @@ services.AddSingleton(); | Date (UTC) | Update | Owner | | --- | --- | --- | | 2026-01-04 | Sprint created; deferred from SPRINT_20251229_049_BE MAINT tasks | Planning | +| 2026-01-04 | DET-001: Audit complete. Found 1526 DateTimeOffset.UtcNow, 181 DateTime.UtcNow, 687 Guid.NewGuid, 16 Random.Shared | Agent | +| 2026-01-04 | DET-002: Created IGuidProvider, SystemGuidProvider, SequentialGuidProvider in StellaOps.Determinism.Abstractions | Agent | +| 2026-01-04 | DET-003: Created DeterminismServiceCollectionExtensions with AddDeterminismDefaults() | Agent | +| 2026-01-04 | DET-004: Policy.Unknowns refactored - UnknownsRepository, BudgetExceededEventFactory, ServiceCollectionExtensions | Agent | +| 2026-01-04 | Fixed Policy.Exceptions csproj - added ImplicitUsings, Nullable, PackageReferences | Agent | ## Decisions & Risks - **Decision:** Defer determinism refactoring from MAINT audit to dedicated sprint for focused, systematic approach. diff --git a/src/Policy/__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj b/src/Policy/__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj index 43c120140..ab1f7e918 100644 --- a/src/Policy/__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj +++ b/src/Policy/__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj @@ -1,6 +1,8 @@ net10.0 + enable + enable true StellaOps.Policy.Exceptions StellaOps @@ -12,4 +14,9 @@ false true + + + + + diff --git a/src/Policy/__Libraries/StellaOps.Policy.Unknowns/Events/BudgetExceededEventFactory.cs b/src/Policy/__Libraries/StellaOps.Policy.Unknowns/Events/BudgetExceededEventFactory.cs index 206bd0cb0..ab749dcf4 100644 --- a/src/Policy/__Libraries/StellaOps.Policy.Unknowns/Events/BudgetExceededEventFactory.cs +++ b/src/Policy/__Libraries/StellaOps.Policy.Unknowns/Events/BudgetExceededEventFactory.cs @@ -29,9 +29,15 @@ public static class BudgetExceededEventFactory /// /// Creates a budget exceeded notification event payload. /// + /// The environment name. + /// The budget check result. + /// Time provider for timestamp. Uses system time if null. + /// Optional image digest. + /// Optional policy revision ID. public static BudgetEventPayload CreatePayload( string environment, BudgetCheckResult result, + TimeProvider? timeProvider = null, string? imageDigest = null, string? policyRevisionId = null) { @@ -57,7 +63,7 @@ public static class BudgetExceededEventFactory Message = result.Message, ImageDigest = imageDigest, PolicyRevisionId = policyRevisionId, - Timestamp = DateTimeOffset.UtcNow + Timestamp = (timeProvider ?? TimeProvider.System).GetUtcNow() }; } @@ -198,7 +204,7 @@ public sealed record BudgetEventPayload /// /// Event timestamp. /// - public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow; + public DateTimeOffset Timestamp { get; init; } } /// diff --git a/src/Policy/__Libraries/StellaOps.Policy.Unknowns/Repositories/UnknownsRepository.cs b/src/Policy/__Libraries/StellaOps.Policy.Unknowns/Repositories/UnknownsRepository.cs index 6ed7e4462..e9c7ba08c 100644 --- a/src/Policy/__Libraries/StellaOps.Policy.Unknowns/Repositories/UnknownsRepository.cs +++ b/src/Policy/__Libraries/StellaOps.Policy.Unknowns/Repositories/UnknownsRepository.cs @@ -1,6 +1,7 @@ using System.Data; using System.Text.Json; using Dapper; +using StellaOps.Determinism; using StellaOps.Policy.Unknowns.Models; namespace StellaOps.Policy.Unknowns.Repositories; @@ -15,9 +16,15 @@ namespace StellaOps.Policy.Unknowns.Repositories; public sealed class UnknownsRepository : IUnknownsRepository { private readonly IDbConnection _connection; + private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; - public UnknownsRepository(IDbConnection connection) - => _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + public UnknownsRepository(IDbConnection connection, TimeProvider timeProvider, IGuidProvider guidProvider) + { + _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider)); + } /// public async Task GetByIdAsync(Guid tenantId, Guid id, CancellationToken cancellationToken = default) @@ -162,8 +169,8 @@ public sealed class UnknownsRepository : IUnknownsRepository /// public async Task CreateAsync(Unknown unknown, CancellationToken cancellationToken = default) { - var id = unknown.Id == Guid.Empty ? Guid.NewGuid() : unknown.Id; - var now = DateTimeOffset.UtcNow; + var id = unknown.Id == Guid.Empty ? _guidProvider.NewGuid() : unknown.Id; + var now = _timeProvider.GetUtcNow(); const string sql = """ SELECT set_config('app.current_tenant', @TenantId::text, true); @@ -258,7 +265,7 @@ public sealed class UnknownsRepository : IUnknownsRepository WHERE id = @Id; """; - var evaluatedAt = DateTimeOffset.UtcNow; + var evaluatedAt = _timeProvider.GetUtcNow(); var param = new { unknown.TenantId, @@ -304,7 +311,7 @@ public sealed class UnknownsRepository : IUnknownsRepository WHERE id = @Id; """; - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var param = new { TenantId = tenantId, @@ -324,7 +331,7 @@ public sealed class UnknownsRepository : IUnknownsRepository IEnumerable unknowns, CancellationToken cancellationToken = default) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); var total = 0; const string sql = """ @@ -370,7 +377,7 @@ public sealed class UnknownsRepository : IUnknownsRepository foreach (var unknown in unknowns) { - var id = unknown.Id == Guid.Empty ? Guid.NewGuid() : unknown.Id; + var id = unknown.Id == Guid.Empty ? _guidProvider.NewGuid() : unknown.Id; var param = new { Id = id, diff --git a/src/Policy/__Libraries/StellaOps.Policy.Unknowns/ServiceCollectionExtensions.cs b/src/Policy/__Libraries/StellaOps.Policy.Unknowns/ServiceCollectionExtensions.cs index d10b34558..e89c7e40c 100644 --- a/src/Policy/__Libraries/StellaOps.Policy.Unknowns/ServiceCollectionExtensions.cs +++ b/src/Policy/__Libraries/StellaOps.Policy.Unknowns/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using StellaOps.Determinism; using StellaOps.Policy.Unknowns.Configuration; using StellaOps.Policy.Unknowns.Repositories; using StellaOps.Policy.Unknowns.Services; @@ -27,6 +28,9 @@ public static class ServiceCollectionExtensions if (configureBudgetOptions is not null) services.Configure(configureBudgetOptions); + // Ensure determinism dependencies are registered + services.AddDeterminismDefaults(); + // Register services services.AddSingleton(); services.AddSingleton(); diff --git a/src/Policy/__Libraries/StellaOps.Policy.Unknowns/StellaOps.Policy.Unknowns.csproj b/src/Policy/__Libraries/StellaOps.Policy.Unknowns/StellaOps.Policy.Unknowns.csproj index 20c38d054..90e7032bc 100644 --- a/src/Policy/__Libraries/StellaOps.Policy.Unknowns/StellaOps.Policy.Unknowns.csproj +++ b/src/Policy/__Libraries/StellaOps.Policy.Unknowns/StellaOps.Policy.Unknowns.csproj @@ -18,6 +18,7 @@ + diff --git a/src/__Libraries/StellaOps.Determinism.Abstractions/DeterminismServiceCollectionExtensions.cs b/src/__Libraries/StellaOps.Determinism.Abstractions/DeterminismServiceCollectionExtensions.cs new file mode 100644 index 000000000..dcb23e6eb --- /dev/null +++ b/src/__Libraries/StellaOps.Determinism.Abstractions/DeterminismServiceCollectionExtensions.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace StellaOps.Determinism; + +/// +/// Extension methods for registering determinism abstractions in DI. +/// +public static class DeterminismServiceCollectionExtensions +{ + /// + /// Adds as a singleton. + /// + public static IServiceCollection AddSystemTimeProvider(this IServiceCollection services) + { + services.TryAddSingleton(TimeProvider.System); + return services; + } + + /// + /// Adds as the singleton. + /// + public static IServiceCollection AddSystemGuidProvider(this IServiceCollection services) + { + services.TryAddSingleton(); + return services; + } + + /// + /// Adds both and as singletons. + /// This is the recommended setup for production services. + /// + public static IServiceCollection AddDeterminismDefaults(this IServiceCollection services) + { + services.AddSystemTimeProvider(); + services.AddSystemGuidProvider(); + return services; + } +} diff --git a/src/__Libraries/StellaOps.Determinism.Abstractions/IGuidProvider.cs b/src/__Libraries/StellaOps.Determinism.Abstractions/IGuidProvider.cs new file mode 100644 index 000000000..6f906b00a --- /dev/null +++ b/src/__Libraries/StellaOps.Determinism.Abstractions/IGuidProvider.cs @@ -0,0 +1,64 @@ +namespace StellaOps.Determinism; + +/// +/// Abstraction for GUID generation to support deterministic testing. +/// Inject this instead of using directly. +/// +public interface IGuidProvider +{ + /// + /// Generates a new GUID. + /// + Guid NewGuid(); +} + +/// +/// Default implementation using . +/// Register as singleton in DI: services.AddSingleton<IGuidProvider, SystemGuidProvider>(); +/// +public sealed class SystemGuidProvider : IGuidProvider +{ + /// + /// Shared instance for non-DI scenarios. + /// + public static readonly SystemGuidProvider Instance = new(); + + /// + public Guid NewGuid() => Guid.NewGuid(); +} + +/// +/// Deterministic GUID provider for testing. Returns GUIDs in a predictable sequence. +/// +public sealed class SequentialGuidProvider : IGuidProvider +{ + private int _counter; + private readonly Guid _baseGuid; + + /// + /// Creates a sequential GUID provider starting from a base GUID. + /// + /// Optional base GUID. Defaults to all zeros. + public SequentialGuidProvider(Guid? baseGuid = null) + { + _baseGuid = baseGuid ?? Guid.Empty; + } + + /// + public Guid NewGuid() + { + var bytes = _baseGuid.ToByteArray(); + var counter = Interlocked.Increment(ref _counter); + // Embed counter in last 4 bytes + bytes[12] = (byte)(counter >> 24); + bytes[13] = (byte)(counter >> 16); + bytes[14] = (byte)(counter >> 8); + bytes[15] = (byte)counter; + return new Guid(bytes); + } + + /// + /// Resets the counter to zero. + /// + public void Reset() => Interlocked.Exchange(ref _counter, 0); +} diff --git a/src/__Libraries/StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj b/src/__Libraries/StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj index aa03be227..f78b37c8b 100644 --- a/src/__Libraries/StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj +++ b/src/__Libraries/StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj @@ -9,4 +9,8 @@ Attributes and abstractions for determinism enforcement in StellaOps. true + + + +