part #2
This commit is contained in:
8
src/__Libraries/StellaOps.Replay/Engine/IFeedLoader.cs
Normal file
8
src/__Libraries/StellaOps.Replay/Engine/IFeedLoader.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Replay.Engine;
|
||||
|
||||
public interface IFeedLoader
|
||||
{
|
||||
Task<FeedSnapshot> LoadByDigestAsync(string digest, CancellationToken ct = default);
|
||||
}
|
||||
8
src/__Libraries/StellaOps.Replay/Engine/IPolicyLoader.cs
Normal file
8
src/__Libraries/StellaOps.Replay/Engine/IPolicyLoader.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Replay.Engine;
|
||||
|
||||
public interface IPolicyLoader
|
||||
{
|
||||
Task<PolicySnapshot> LoadByDigestAsync(string digest, CancellationToken ct = default);
|
||||
}
|
||||
10
src/__Libraries/StellaOps.Replay/Engine/IReplayEngine.cs
Normal file
10
src/__Libraries/StellaOps.Replay/Engine/IReplayEngine.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using StellaOps.Replay.Models;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Replay.Engine;
|
||||
|
||||
public interface IReplayEngine
|
||||
{
|
||||
Task<ReplayResult> ReplayAsync(RunManifest manifest, ReplayOptions options, CancellationToken ct = default);
|
||||
DeterminismCheckResult CheckDeterminism(ReplayResult a, ReplayResult b);
|
||||
}
|
||||
10
src/__Libraries/StellaOps.Replay/Engine/IScanner.cs
Normal file
10
src/__Libraries/StellaOps.Replay/Engine/IScanner.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using StellaOps.Replay.Models;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Replay.Engine;
|
||||
|
||||
public interface IScanner
|
||||
{
|
||||
Task<ScanResult> ScanAsync(ImmutableArray<ArtifactDigest> artifacts, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using StellaOps.Replay.Models;
|
||||
|
||||
namespace StellaOps.Replay.Engine;
|
||||
|
||||
public interface IScannerFactory
|
||||
{
|
||||
IScanner Create(ScannerOptions options);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using StellaOps.Replay.Models;
|
||||
|
||||
namespace StellaOps.Replay.Engine;
|
||||
|
||||
public sealed partial class ReplayEngine
|
||||
{
|
||||
public DeterminismCheckResult CheckDeterminism(ReplayResult a, ReplayResult b)
|
||||
{
|
||||
if (a.VerdictDigest == b.VerdictDigest)
|
||||
{
|
||||
return new DeterminismCheckResult
|
||||
{
|
||||
IsDeterministic = true,
|
||||
DigestA = a.VerdictDigest,
|
||||
DigestB = b.VerdictDigest,
|
||||
Differences = []
|
||||
};
|
||||
}
|
||||
|
||||
var differences = FindJsonDifferences(a.VerdictJson, b.VerdictJson);
|
||||
return new DeterminismCheckResult
|
||||
{
|
||||
IsDeterministic = false,
|
||||
DigestA = a.VerdictDigest,
|
||||
DigestB = b.VerdictDigest,
|
||||
Differences = differences
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using StellaOps.Canonicalization.Verification;
|
||||
using StellaOps.Replay.Models;
|
||||
|
||||
namespace StellaOps.Replay.Engine;
|
||||
|
||||
public sealed partial class ReplayEngine
|
||||
{
|
||||
private static IReadOnlyList<JsonDifference> FindJsonDifferences(string? a, string? b)
|
||||
{
|
||||
if (a is null || b is null)
|
||||
return [new JsonDifference("$", "One or both values are null")];
|
||||
|
||||
var verifier = new DeterminismVerifier();
|
||||
var result = verifier.Compare(a, b);
|
||||
return result.Differences.Select(d => new JsonDifference(d, "Value mismatch")).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using StellaOps.Replay.Models;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Replay.Engine;
|
||||
|
||||
public sealed partial class ReplayEngine
|
||||
{
|
||||
private async Task<LoadResult<FeedSnapshot>> LoadFeedSnapshotAsync(
|
||||
FeedSnapshot snapshot,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var feed = await _feedLoader.LoadByDigestAsync(snapshot.Digest, ct).ConfigureAwait(false);
|
||||
if (!string.Equals(feed.Digest, snapshot.Digest, StringComparison.OrdinalIgnoreCase))
|
||||
return LoadResult<FeedSnapshot>.Fail($"Feed digest mismatch: expected {snapshot.Digest}");
|
||||
return LoadResult<FeedSnapshot>.Ok(feed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return LoadResult<FeedSnapshot>.Fail($"Failed to load feed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LoadResult<PolicySnapshot>> LoadPolicySnapshotAsync(
|
||||
PolicySnapshot snapshot,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var policy = await _policyLoader.LoadByDigestAsync(snapshot.LatticeRulesDigest, ct)
|
||||
.ConfigureAwait(false);
|
||||
return LoadResult<PolicySnapshot>.Ok(policy);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return LoadResult<PolicySnapshot>.Fail($"Failed to load policy: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using StellaOps.Replay.Models;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Replay.Engine;
|
||||
|
||||
public sealed partial class ReplayEngine
|
||||
{
|
||||
private static ValidationResult ValidateManifest(RunManifest manifest)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(manifest.RunId))
|
||||
errors.Add("RunId is required");
|
||||
|
||||
if (manifest.ArtifactDigests.Length == 0)
|
||||
errors.Add("At least one artifact digest required");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(manifest.FeedSnapshot.Digest))
|
||||
errors.Add("Feed snapshot digest required");
|
||||
|
||||
return new ValidationResult(errors.Count == 0, errors);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Canonicalization.Json;
|
||||
using StellaOps.Canonicalization.Verification;
|
||||
using StellaOps.Replay.Models;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Replay.Engine;
|
||||
|
||||
@@ -12,7 +10,7 @@ namespace StellaOps.Replay.Engine;
|
||||
/// Executes scans deterministically from run manifests.
|
||||
/// Enables time-travel replay for verification and auditing.
|
||||
/// </summary>
|
||||
public sealed class ReplayEngine : IReplayEngine
|
||||
public sealed partial class ReplayEngine : IReplayEngine
|
||||
{
|
||||
private readonly IFeedLoader _feedLoader;
|
||||
private readonly IPolicyLoader _policyLoader;
|
||||
@@ -44,16 +42,32 @@ public sealed class ReplayEngine : IReplayEngine
|
||||
var validationResult = ValidateManifest(manifest);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
return ReplayResult.Failed(manifest.RunId, "Manifest validation failed", validationResult.Errors);
|
||||
return ReplayResult.Failed(
|
||||
manifest.RunId,
|
||||
"Manifest validation failed",
|
||||
validationResult.Errors,
|
||||
_timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
var feedResult = await LoadFeedSnapshotAsync(manifest.FeedSnapshot, ct).ConfigureAwait(false);
|
||||
if (!feedResult.Success)
|
||||
return ReplayResult.Failed(manifest.RunId, "Failed to load feed snapshot", [feedResult.Error ?? "Unknown error"]);
|
||||
{
|
||||
return ReplayResult.Failed(
|
||||
manifest.RunId,
|
||||
"Failed to load feed snapshot",
|
||||
[feedResult.Error ?? "Unknown error"],
|
||||
_timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
var policyResult = await LoadPolicySnapshotAsync(manifest.PolicySnapshot, ct).ConfigureAwait(false);
|
||||
if (!policyResult.Success)
|
||||
return ReplayResult.Failed(manifest.RunId, "Failed to load policy snapshot", [policyResult.Error ?? "Unknown error"]);
|
||||
{
|
||||
return ReplayResult.Failed(
|
||||
manifest.RunId,
|
||||
"Failed to load policy snapshot",
|
||||
[policyResult.Error ?? "Unknown error"],
|
||||
_timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
var scannerOptions = new ScannerOptions
|
||||
{
|
||||
@@ -81,109 +95,4 @@ public sealed class ReplayEngine : IReplayEngine
|
||||
DurationMs = scanResult.DurationMs
|
||||
};
|
||||
}
|
||||
|
||||
public DeterminismCheckResult CheckDeterminism(ReplayResult a, ReplayResult b)
|
||||
{
|
||||
if (a.VerdictDigest == b.VerdictDigest)
|
||||
{
|
||||
return new DeterminismCheckResult
|
||||
{
|
||||
IsDeterministic = true,
|
||||
DigestA = a.VerdictDigest,
|
||||
DigestB = b.VerdictDigest,
|
||||
Differences = []
|
||||
};
|
||||
}
|
||||
|
||||
var differences = FindJsonDifferences(a.VerdictJson, b.VerdictJson);
|
||||
return new DeterminismCheckResult
|
||||
{
|
||||
IsDeterministic = false,
|
||||
DigestA = a.VerdictDigest,
|
||||
DigestB = b.VerdictDigest,
|
||||
Differences = differences
|
||||
};
|
||||
}
|
||||
|
||||
private static ValidationResult ValidateManifest(RunManifest manifest)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(manifest.RunId))
|
||||
errors.Add("RunId is required");
|
||||
|
||||
if (manifest.ArtifactDigests.Length == 0)
|
||||
errors.Add("At least one artifact digest required");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(manifest.FeedSnapshot.Digest))
|
||||
errors.Add("Feed snapshot digest required");
|
||||
|
||||
return new ValidationResult(errors.Count == 0, errors);
|
||||
}
|
||||
|
||||
private async Task<LoadResult<FeedSnapshot>> LoadFeedSnapshotAsync(
|
||||
FeedSnapshot snapshot, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var feed = await _feedLoader.LoadByDigestAsync(snapshot.Digest, ct).ConfigureAwait(false);
|
||||
if (!string.Equals(feed.Digest, snapshot.Digest, StringComparison.OrdinalIgnoreCase))
|
||||
return LoadResult<FeedSnapshot>.Fail($"Feed digest mismatch: expected {snapshot.Digest}");
|
||||
return LoadResult<FeedSnapshot>.Ok(feed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return LoadResult<FeedSnapshot>.Fail($"Failed to load feed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LoadResult<PolicySnapshot>> LoadPolicySnapshotAsync(
|
||||
PolicySnapshot snapshot, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var policy = await _policyLoader.LoadByDigestAsync(snapshot.LatticeRulesDigest, ct).ConfigureAwait(false);
|
||||
return LoadResult<PolicySnapshot>.Ok(policy);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return LoadResult<PolicySnapshot>.Fail($"Failed to load policy: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<JsonDifference> FindJsonDifferences(string? a, string? b)
|
||||
{
|
||||
if (a is null || b is null)
|
||||
return [new JsonDifference("$", "One or both values are null")];
|
||||
|
||||
var verifier = new DeterminismVerifier();
|
||||
var result = verifier.Compare(a, b);
|
||||
return result.Differences.Select(d => new JsonDifference(d, "Value mismatch")).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public interface IReplayEngine
|
||||
{
|
||||
Task<ReplayResult> ReplayAsync(RunManifest manifest, ReplayOptions options, CancellationToken ct = default);
|
||||
DeterminismCheckResult CheckDeterminism(ReplayResult a, ReplayResult b);
|
||||
}
|
||||
|
||||
public interface IScannerFactory
|
||||
{
|
||||
IScanner Create(ScannerOptions options);
|
||||
}
|
||||
|
||||
public interface IScanner
|
||||
{
|
||||
Task<ScanResult> ScanAsync(ImmutableArray<ArtifactDigest> artifacts, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public interface IFeedLoader
|
||||
{
|
||||
Task<FeedSnapshot> LoadByDigestAsync(string digest, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public interface IPolicyLoader
|
||||
{
|
||||
Task<PolicySnapshot> LoadByDigestAsync(string digest, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace StellaOps.Replay.Loaders;
|
||||
|
||||
public sealed class DigestMismatchException : Exception
|
||||
{
|
||||
public DigestMismatchException(string message) : base(message) { }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace StellaOps.Replay.Loaders;
|
||||
|
||||
public sealed class FeedNotFoundException : Exception
|
||||
{
|
||||
public FeedNotFoundException(string message) : base(message) { }
|
||||
}
|
||||
@@ -21,24 +21,25 @@ public sealed class FeedSnapshotLoader : IFeedLoader
|
||||
|
||||
public async Task<FeedSnapshot> LoadByDigestAsync(string digest, CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug("Loading feed snapshot with digest {Digest}", digest);
|
||||
var normalizedDigest = SnapshotDigestGuard.EnsureSha256Hex(digest, nameof(digest));
|
||||
_logger.LogDebug("Loading feed snapshot with digest {Digest}", normalizedDigest);
|
||||
|
||||
var localPath = GetLocalPath(digest);
|
||||
var localPath = GetLocalPath(normalizedDigest);
|
||||
if (File.Exists(localPath))
|
||||
{
|
||||
var feed = await LoadFromFileAsync(localPath, ct).ConfigureAwait(false);
|
||||
VerifyDigest(feed, digest);
|
||||
VerifyDigest(feed, normalizedDigest);
|
||||
return feed;
|
||||
}
|
||||
|
||||
var storedFeed = await _storage.GetByDigestAsync(digest, ct).ConfigureAwait(false);
|
||||
var storedFeed = await _storage.GetByDigestAsync(normalizedDigest, ct).ConfigureAwait(false);
|
||||
if (storedFeed is not null)
|
||||
{
|
||||
VerifyDigest(storedFeed, digest);
|
||||
VerifyDigest(storedFeed, normalizedDigest);
|
||||
return storedFeed;
|
||||
}
|
||||
|
||||
throw new FeedNotFoundException($"Feed snapshot not found: {digest}");
|
||||
throw new FeedNotFoundException($"Feed snapshot not found: {normalizedDigest}");
|
||||
}
|
||||
|
||||
private static void VerifyDigest(FeedSnapshot feed, string expected)
|
||||
@@ -66,18 +67,3 @@ public sealed class FeedSnapshotLoader : IFeedLoader
|
||||
return CanonicalJsonSerializer.Deserialize<FeedSnapshot>(json);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IFeedStorage
|
||||
{
|
||||
Task<FeedSnapshot?> GetByDigestAsync(string digest, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed class FeedNotFoundException : Exception
|
||||
{
|
||||
public FeedNotFoundException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
public sealed class DigestMismatchException : Exception
|
||||
{
|
||||
public DigestMismatchException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
8
src/__Libraries/StellaOps.Replay/Loaders/IFeedStorage.cs
Normal file
8
src/__Libraries/StellaOps.Replay/Loaders/IFeedStorage.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Replay.Loaders;
|
||||
|
||||
public interface IFeedStorage
|
||||
{
|
||||
Task<FeedSnapshot?> GetByDigestAsync(string digest, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Replay.Loaders;
|
||||
|
||||
public interface IPolicyStorage
|
||||
{
|
||||
Task<PolicySnapshot?> GetByDigestAsync(string digest, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace StellaOps.Replay.Loaders;
|
||||
|
||||
public sealed class PolicyNotFoundException : Exception
|
||||
{
|
||||
public PolicyNotFoundException(string message) : base(message) { }
|
||||
}
|
||||
@@ -21,24 +21,25 @@ public sealed class PolicySnapshotLoader : IPolicyLoader
|
||||
|
||||
public async Task<PolicySnapshot> LoadByDigestAsync(string digest, CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug("Loading policy snapshot with digest {Digest}", digest);
|
||||
var normalizedDigest = SnapshotDigestGuard.EnsureSha256Hex(digest, nameof(digest));
|
||||
_logger.LogDebug("Loading policy snapshot with digest {Digest}", normalizedDigest);
|
||||
|
||||
var localPath = GetLocalPath(digest);
|
||||
var localPath = GetLocalPath(normalizedDigest);
|
||||
if (File.Exists(localPath))
|
||||
{
|
||||
var policy = await LoadFromFileAsync(localPath, ct).ConfigureAwait(false);
|
||||
VerifyDigest(policy, digest);
|
||||
VerifyDigest(policy, normalizedDigest);
|
||||
return policy;
|
||||
}
|
||||
|
||||
var stored = await _storage.GetByDigestAsync(digest, ct).ConfigureAwait(false);
|
||||
var stored = await _storage.GetByDigestAsync(normalizedDigest, ct).ConfigureAwait(false);
|
||||
if (stored is not null)
|
||||
{
|
||||
VerifyDigest(stored, digest);
|
||||
VerifyDigest(stored, normalizedDigest);
|
||||
return stored;
|
||||
}
|
||||
|
||||
throw new PolicyNotFoundException($"Policy snapshot not found: {digest}");
|
||||
throw new PolicyNotFoundException($"Policy snapshot not found: {normalizedDigest}");
|
||||
}
|
||||
|
||||
private static void VerifyDigest(PolicySnapshot policy, string expected)
|
||||
@@ -66,13 +67,3 @@ public sealed class PolicySnapshotLoader : IPolicyLoader
|
||||
return CanonicalJsonSerializer.Deserialize<PolicySnapshot>(json);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IPolicyStorage
|
||||
{
|
||||
Task<PolicySnapshot?> GetByDigestAsync(string digest, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed class PolicyNotFoundException : Exception
|
||||
{
|
||||
public PolicyNotFoundException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace StellaOps.Replay.Loaders;
|
||||
|
||||
internal static class SnapshotDigestGuard
|
||||
{
|
||||
private const int HexLength = 64;
|
||||
|
||||
internal static string EnsureSha256Hex(string digest, string parameterName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(digest))
|
||||
throw new ArgumentException($"{parameterName} is required.", parameterName);
|
||||
|
||||
var trimmed = digest.Trim();
|
||||
if (trimmed.Length != HexLength || !IsHex(trimmed))
|
||||
throw new FormatException($"{parameterName} must be {HexLength} hexadecimal characters.");
|
||||
|
||||
return trimmed.ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool IsHex(ReadOnlySpan<char> value)
|
||||
{
|
||||
foreach (var c in value)
|
||||
{
|
||||
if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -9,3 +9,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0108-T | DONE | Revalidated 2026-01-08; test coverage audit for Replay library. |
|
||||
| AUDIT-0108-A | TODO | Pending approval (revalidated 2026-01-08). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| REMED-07 | DONE | 2026-02-05; ReplayEngine split into partials/interfaces, loader digest guard and exceptions separated, failure timestamps use TimeProvider; dotnet test src/__Libraries/__Tests/StellaOps.Replay.Tests/StellaOps.Replay.Tests.csproj passed (11 tests). |
|
||||
|
||||
Reference in New Issue
Block a user