using System.Collections.Concurrent;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Canonical.Json;
namespace StellaOps.Testing.Determinism;
///
/// Stores and retrieves determinism baselines for artifact comparison.
/// Baselines are SHA-256 hashes of canonical artifact representations used to detect drift.
///
public sealed class DeterminismBaselineStore
{
private readonly string _baselineDirectory;
private readonly ConcurrentDictionary _cache = new();
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
///
/// Creates a baseline store with the specified directory.
///
/// Directory path for storing baselines.
public DeterminismBaselineStore(string baselineDirectory)
{
ArgumentException.ThrowIfNullOrWhiteSpace(baselineDirectory);
_baselineDirectory = baselineDirectory;
}
///
/// Creates a baseline store using the default baseline directory.
/// Default: tests/baselines/determinism relative to repository root.
///
/// Repository root directory.
/// Configured baseline store.
public static DeterminismBaselineStore CreateDefault(string repositoryRoot)
{
ArgumentException.ThrowIfNullOrWhiteSpace(repositoryRoot);
var baselineDir = Path.Combine(repositoryRoot, "tests", "baselines", "determinism");
return new DeterminismBaselineStore(baselineDir);
}
///
/// Stores a baseline for an artifact.
///
/// Type of artifact (e.g., "sbom", "vex", "policy-verdict").
/// Name of the artifact (e.g., "alpine-3.18-spdx").
/// The baseline to store.
/// Cancellation token.
public async Task StoreBaselineAsync(
string artifactType,
string artifactName,
DeterminismBaseline baseline,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(artifactType);
ArgumentException.ThrowIfNullOrWhiteSpace(artifactName);
ArgumentNullException.ThrowIfNull(baseline);
var key = GetBaselineKey(artifactType, artifactName);
var filePath = GetBaselineFilePath(artifactType, artifactName);
// Ensure directory exists
var directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
// Serialize and write
var json = JsonSerializer.Serialize(baseline, JsonOptions);
await File.WriteAllTextAsync(filePath, json, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
// Update cache
_cache[key] = baseline;
}
///
/// Retrieves a baseline for an artifact.
///
/// Type of artifact.
/// Name of the artifact.
/// Cancellation token.
/// The baseline if found, null otherwise.
public async Task GetBaselineAsync(
string artifactType,
string artifactName,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(artifactType);
ArgumentException.ThrowIfNullOrWhiteSpace(artifactName);
var key = GetBaselineKey(artifactType, artifactName);
// Check cache first
if (_cache.TryGetValue(key, out var cached))
{
return cached;
}
// Load from file
var filePath = GetBaselineFilePath(artifactType, artifactName);
if (!File.Exists(filePath))
{
return null;
}
var json = await File.ReadAllTextAsync(filePath, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
var baseline = JsonSerializer.Deserialize(json, JsonOptions);
if (baseline is not null)
{
_cache[key] = baseline;
}
return baseline;
}
///
/// Compares an artifact against its stored baseline.
///
/// Type of artifact.
/// Name of the artifact.
/// Current SHA-256 hash of the artifact.
/// Cancellation token.
/// Comparison result indicating match, drift, or missing baseline.
public async Task CompareAsync(
string artifactType,
string artifactName,
string currentHash,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(artifactType);
ArgumentException.ThrowIfNullOrWhiteSpace(artifactName);
ArgumentException.ThrowIfNullOrWhiteSpace(currentHash);
var baseline = await GetBaselineAsync(artifactType, artifactName, cancellationToken).ConfigureAwait(false);
if (baseline is null)
{
return new BaselineComparisonResult
{
ArtifactType = artifactType,
ArtifactName = artifactName,
Status = BaselineStatus.Missing,
CurrentHash = currentHash,
BaselineHash = null,
Message = $"No baseline found for {artifactType}/{artifactName}. Run with UPDATE_BASELINES=true to create."
};
}
var isMatch = string.Equals(baseline.CanonicalHash, currentHash, StringComparison.OrdinalIgnoreCase);
return new BaselineComparisonResult
{
ArtifactType = artifactType,
ArtifactName = artifactName,
Status = isMatch ? BaselineStatus.Match : BaselineStatus.Drift,
CurrentHash = currentHash,
BaselineHash = baseline.CanonicalHash,
BaselineVersion = baseline.Version,
Message = isMatch
? $"Artifact {artifactType}/{artifactName} matches baseline."
: $"DRIFT DETECTED: {artifactType}/{artifactName} hash changed from {baseline.CanonicalHash} to {currentHash}."
};
}
///
/// Lists all baselines in the store.
///
/// Cancellation token.
/// Collection of baseline entries.
public async Task> ListBaselinesAsync(
CancellationToken cancellationToken = default)
{
var entries = new List();
if (!Directory.Exists(_baselineDirectory))
{
return entries;
}
var files = Directory.GetFiles(_baselineDirectory, "*.baseline.json", SearchOption.AllDirectories);
foreach (var file in files)
{
try
{
var json = await File.ReadAllTextAsync(file, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
var baseline = JsonSerializer.Deserialize(json, JsonOptions);
if (baseline is not null)
{
var relativePath = Path.GetRelativePath(_baselineDirectory, file);
var parts = relativePath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
entries.Add(new BaselineEntry
{
ArtifactType = parts.Length > 1 ? parts[0] : "unknown",
ArtifactName = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(file)),
CanonicalHash = baseline.CanonicalHash,
Version = baseline.Version,
UpdatedAt = baseline.UpdatedAt,
FilePath = file
});
}
}
catch
{
// Skip invalid baseline files
}
}
return entries.OrderBy(e => e.ArtifactType).ThenBy(e => e.ArtifactName).ToList();
}
///
/// Creates a baseline from an artifact.
///
/// The artifact bytes to hash.
/// Version identifier for this baseline.
/// Optional metadata about the baseline.
/// Created baseline.
public static DeterminismBaseline CreateBaseline(
ReadOnlySpan artifactBytes,
string version,
IReadOnlyDictionary? metadata = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(version);
var hash = CanonJson.Sha256Hex(artifactBytes);
return new DeterminismBaseline
{
CanonicalHash = hash,
Algorithm = "SHA-256",
Version = version,
UpdatedAt = DateTimeOffset.UtcNow,
Metadata = metadata
};
}
///
/// Creates a baseline from a JSON artifact with canonical serialization.
///
/// The artifact type.
/// The artifact to serialize and hash.
/// Version identifier for this baseline.
/// Optional metadata about the baseline.
/// Created baseline.
public static DeterminismBaseline CreateBaselineFromJson(
T artifact,
string version,
IReadOnlyDictionary? metadata = null)
{
ArgumentNullException.ThrowIfNull(artifact);
ArgumentException.ThrowIfNullOrWhiteSpace(version);
var canonicalBytes = CanonJson.Canonicalize(artifact);
var hash = CanonJson.Sha256Hex(canonicalBytes);
return new DeterminismBaseline
{
CanonicalHash = hash,
Algorithm = "SHA-256",
Version = version,
UpdatedAt = DateTimeOffset.UtcNow,
Metadata = metadata
};
}
///
/// Gets the baseline directory path.
///
public string BaselineDirectory => _baselineDirectory;
private string GetBaselineFilePath(string artifactType, string artifactName)
{
var safeType = SanitizePathComponent(artifactType);
var safeName = SanitizePathComponent(artifactName);
return Path.Combine(_baselineDirectory, safeType, $"{safeName}.baseline.json");
}
private static string GetBaselineKey(string artifactType, string artifactName)
{
return $"{artifactType}/{artifactName}".ToLowerInvariant();
}
private static string SanitizePathComponent(string component)
{
var invalid = Path.GetInvalidFileNameChars();
var sanitized = new StringBuilder(component.Length);
foreach (var c in component)
{
sanitized.Append(invalid.Contains(c) ? '_' : c);
}
return sanitized.ToString();
}
}
///
/// A stored baseline for determinism comparison.
///
public sealed record DeterminismBaseline
{
///
/// SHA-256 hash of the canonical artifact representation (hex-encoded).
///
[JsonPropertyName("canonicalHash")]
public required string CanonicalHash { get; init; }
///
/// Hash algorithm used (always "SHA-256").
///
[JsonPropertyName("algorithm")]
public required string Algorithm { get; init; }
///
/// Version identifier for this baseline (e.g., "1.0.0", git SHA, or timestamp).
///
[JsonPropertyName("version")]
public required string Version { get; init; }
///
/// UTC timestamp when this baseline was created or updated.
///
[JsonPropertyName("updatedAt")]
public required DateTimeOffset UpdatedAt { get; init; }
///
/// Optional metadata about the baseline.
///
[JsonPropertyName("metadata")]
public IReadOnlyDictionary? Metadata { get; init; }
}
///
/// Result of comparing an artifact against its baseline.
///
public sealed record BaselineComparisonResult
{
///
/// Type of artifact compared.
///
[JsonPropertyName("artifactType")]
public required string ArtifactType { get; init; }
///
/// Name of artifact compared.
///
[JsonPropertyName("artifactName")]
public required string ArtifactName { get; init; }
///
/// Comparison status.
///
[JsonPropertyName("status")]
public required BaselineStatus Status { get; init; }
///
/// Current hash of the artifact.
///
[JsonPropertyName("currentHash")]
public required string CurrentHash { get; init; }
///
/// Baseline hash (null if missing).
///
[JsonPropertyName("baselineHash")]
public string? BaselineHash { get; init; }
///
/// Baseline version (null if missing).
///
[JsonPropertyName("baselineVersion")]
public string? BaselineVersion { get; init; }
///
/// Human-readable message describing the result.
///
[JsonPropertyName("message")]
public required string Message { get; init; }
}
///
/// Status of a baseline comparison.
///
public enum BaselineStatus
{
///
/// Artifact matches baseline hash.
///
Match,
///
/// Artifact hash differs from baseline (drift detected).
///
Drift,
///
/// No baseline exists for this artifact.
///
Missing
}
///
/// Entry in the baseline registry.
///
public sealed record BaselineEntry
{
///
/// Type of artifact.
///
[JsonPropertyName("artifactType")]
public required string ArtifactType { get; init; }
///
/// Name of artifact.
///
[JsonPropertyName("artifactName")]
public required string ArtifactName { get; init; }
///
/// Canonical hash of the baseline.
///
[JsonPropertyName("canonicalHash")]
public required string CanonicalHash { get; init; }
///
/// Version identifier.
///
[JsonPropertyName("version")]
public required string Version { get; init; }
///
/// When baseline was last updated.
///
[JsonPropertyName("updatedAt")]
public required DateTimeOffset UpdatedAt { get; init; }
///
/// File path of the baseline.
///
[JsonPropertyName("filePath")]
public required string FilePath { get; init; }
}