stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
public sealed partial class ReplayToken
|
||||
{
|
||||
public override string ToString() => Canonical;
|
||||
|
||||
public bool Equals(ReplayToken? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is ReplayToken other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
public sealed partial class ReplayToken
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the token has expired.
|
||||
/// </summary>
|
||||
/// <param name="currentTime">The current time to check against. If null, uses DateTimeOffset.UtcNow.</param>
|
||||
/// <returns>True if the token has expired; false if not expired or has no expiration.</returns>
|
||||
public bool IsExpired(DateTimeOffset? currentTime = null)
|
||||
{
|
||||
if (!ExpiresAt.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var now = currentTime ?? DateTimeOffset.UtcNow;
|
||||
return now >= ExpiresAt.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remaining time until expiration.
|
||||
/// </summary>
|
||||
/// <param name="currentTime">The current time to check against. If null, uses DateTimeOffset.UtcNow.</param>
|
||||
/// <returns>The remaining time, or null if no expiration or already expired.</returns>
|
||||
public TimeSpan? GetTimeToExpiration(DateTimeOffset? currentTime = null)
|
||||
{
|
||||
if (!ExpiresAt.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var now = currentTime ?? DateTimeOffset.UtcNow;
|
||||
var remaining = ExpiresAt.Value - now;
|
||||
return remaining > TimeSpan.Zero ? remaining : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
public sealed partial class ReplayToken
|
||||
{
|
||||
/// <summary>
|
||||
/// Parse a canonical token string.
|
||||
/// Supports both v1.0 format (4 parts) and v2.0 format with expiration (5 parts).
|
||||
/// GeneratedAt is set to UnixEpoch because the canonical format does not include it.
|
||||
/// </summary>
|
||||
public static ReplayToken Parse(string canonical)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(canonical))
|
||||
{
|
||||
throw new ArgumentException("Token cannot be empty.", nameof(canonical));
|
||||
}
|
||||
|
||||
var parts = canonical.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length < 4 || parts.Length > 5 || !string.Equals(parts[0], Scheme, StringComparison.Ordinal))
|
||||
{
|
||||
throw new FormatException($"Invalid replay token format: {canonical}");
|
||||
}
|
||||
|
||||
var versionPart = parts[1];
|
||||
if (!versionPart.StartsWith("v", StringComparison.Ordinal) || versionPart.Length <= 1)
|
||||
{
|
||||
throw new FormatException($"Invalid replay token version: {canonical}");
|
||||
}
|
||||
|
||||
var version = versionPart[1..];
|
||||
var algorithm = parts[2];
|
||||
var value = parts[3];
|
||||
|
||||
DateTimeOffset? expiresAt = null;
|
||||
if (parts.Length == 5)
|
||||
{
|
||||
if (!long.TryParse(parts[4], out var unixSeconds))
|
||||
{
|
||||
throw new FormatException($"Invalid expiration timestamp in replay token: {canonical}");
|
||||
}
|
||||
expiresAt = DateTimeOffset.FromUnixTimeSeconds(unixSeconds);
|
||||
}
|
||||
|
||||
return new ReplayToken(value, DateTimeOffset.UnixEpoch, expiresAt, algorithm, version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to parse a canonical token string.
|
||||
/// </summary>
|
||||
/// <returns>True if parsing succeeded; false otherwise.</returns>
|
||||
public static bool TryParse(string canonical, out ReplayToken? token)
|
||||
{
|
||||
try
|
||||
{
|
||||
token = Parse(canonical);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
token = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ namespace StellaOps.Audit.ReplayToken;
|
||||
/// <summary>
|
||||
/// A deterministic, content-addressable replay token with optional expiration.
|
||||
/// </summary>
|
||||
public sealed class ReplayToken : IEquatable<ReplayToken>
|
||||
public sealed partial class ReplayToken : IEquatable<ReplayToken>
|
||||
{
|
||||
public const string Scheme = "replay";
|
||||
public const string DefaultAlgorithm = "SHA-256";
|
||||
@@ -75,7 +75,6 @@ public sealed class ReplayToken : IEquatable<ReplayToken>
|
||||
ExpiresAt = expiresAt;
|
||||
Algorithm = string.IsNullOrWhiteSpace(algorithm) ? DefaultAlgorithm : algorithm.Trim();
|
||||
|
||||
// Default to v2.0 if expiration is set, otherwise use provided or default
|
||||
if (expiresAt.HasValue && string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
Version = VersionWithExpiration;
|
||||
@@ -85,115 +84,4 @@ public sealed class ReplayToken : IEquatable<ReplayToken>
|
||||
Version = string.IsNullOrWhiteSpace(version) ? DefaultVersion : version.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the token has expired.
|
||||
/// </summary>
|
||||
/// <param name="currentTime">The current time to check against. If null, uses DateTimeOffset.UtcNow.</param>
|
||||
/// <returns>True if the token has expired; false if not expired or has no expiration.</returns>
|
||||
public bool IsExpired(DateTimeOffset? currentTime = null)
|
||||
{
|
||||
if (!ExpiresAt.HasValue)
|
||||
{
|
||||
return false; // v1.0 tokens without expiration never expire
|
||||
}
|
||||
|
||||
var now = currentTime ?? DateTimeOffset.UtcNow;
|
||||
return now >= ExpiresAt.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remaining time until expiration.
|
||||
/// </summary>
|
||||
/// <param name="currentTime">The current time to check against. If null, uses DateTimeOffset.UtcNow.</param>
|
||||
/// <returns>The remaining time, or null if no expiration or already expired.</returns>
|
||||
public TimeSpan? GetTimeToExpiration(DateTimeOffset? currentTime = null)
|
||||
{
|
||||
if (!ExpiresAt.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var now = currentTime ?? DateTimeOffset.UtcNow;
|
||||
var remaining = ExpiresAt.Value - now;
|
||||
return remaining > TimeSpan.Zero ? remaining : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a canonical token string.
|
||||
/// Supports both v1.0 format (4 parts) and v2.0 format with expiration (5 parts).
|
||||
/// GeneratedAt is set to UnixEpoch because the canonical format does not include it.
|
||||
/// </summary>
|
||||
public static ReplayToken Parse(string canonical)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(canonical))
|
||||
{
|
||||
throw new ArgumentException("Token cannot be empty.", nameof(canonical));
|
||||
}
|
||||
|
||||
var parts = canonical.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// v1.0 format: replay:v1.0:SHA-256:hash (4 parts)
|
||||
// v2.0 format: replay:v2.0:SHA-256:hash:expiry_unix_seconds (5 parts)
|
||||
if (parts.Length < 4 || parts.Length > 5 || !string.Equals(parts[0], Scheme, StringComparison.Ordinal))
|
||||
{
|
||||
throw new FormatException($"Invalid replay token format: {canonical}");
|
||||
}
|
||||
|
||||
var versionPart = parts[1];
|
||||
if (!versionPart.StartsWith("v", StringComparison.Ordinal) || versionPart.Length <= 1)
|
||||
{
|
||||
throw new FormatException($"Invalid replay token version: {canonical}");
|
||||
}
|
||||
|
||||
var version = versionPart[1..];
|
||||
var algorithm = parts[2];
|
||||
var value = parts[3];
|
||||
|
||||
DateTimeOffset? expiresAt = null;
|
||||
if (parts.Length == 5)
|
||||
{
|
||||
if (!long.TryParse(parts[4], out var unixSeconds))
|
||||
{
|
||||
throw new FormatException($"Invalid expiration timestamp in replay token: {canonical}");
|
||||
}
|
||||
expiresAt = DateTimeOffset.FromUnixTimeSeconds(unixSeconds);
|
||||
}
|
||||
|
||||
return new ReplayToken(value, DateTimeOffset.UnixEpoch, expiresAt, algorithm, version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to parse a canonical token string.
|
||||
/// </summary>
|
||||
/// <returns>True if parsing succeeded; false otherwise.</returns>
|
||||
public static bool TryParse(string canonical, out ReplayToken? token)
|
||||
{
|
||||
try
|
||||
{
|
||||
token = Parse(canonical);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
token = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => Canonical;
|
||||
|
||||
public bool Equals(ReplayToken? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is ReplayToken other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
public sealed partial class Sha256ReplayTokenGenerator
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
private static string? NormalizeValue(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
private static List<string> NormalizeSortedList(IReadOnlyList<string>? values)
|
||||
{
|
||||
if (values is null || values.Count == 0)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
return values
|
||||
.Where(static x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(static x => x.Trim())
|
||||
.OrderBy(static x => x, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
private static Dictionary<string, string> NormalizeSortedDictionary(IReadOnlyDictionary<string, string>? values)
|
||||
{
|
||||
if (values is null || values.Count == 0)
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
var normalized = new List<KeyValuePair<string, string>>(values.Count);
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var kvp in values)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(kvp.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = kvp.Key.Trim();
|
||||
if (!seen.Add(key))
|
||||
{
|
||||
throw new ArgumentException($"AdditionalContext contains duplicate key after normalization: '{key}'.", nameof(values));
|
||||
}
|
||||
|
||||
normalized.Add(new KeyValuePair<string, string>(key, kvp.Value?.Trim() ?? string.Empty));
|
||||
}
|
||||
|
||||
return normalized
|
||||
.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal)
|
||||
.ToDictionary(static kvp => kvp.Key, static kvp => kvp.Value, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Produces deterministic canonical representation of inputs.
|
||||
/// </summary>
|
||||
private static string Canonicalize(ReplayTokenRequest request, string version)
|
||||
{
|
||||
var canonical = new CanonicalReplayInput
|
||||
{
|
||||
Version = version,
|
||||
FeedManifests = NormalizeSortedList(request.FeedManifests),
|
||||
RulesVersion = NormalizeValue(request.RulesVersion),
|
||||
RulesHash = NormalizeValue(request.RulesHash),
|
||||
LatticePolicyVersion = NormalizeValue(request.LatticePolicyVersion),
|
||||
LatticePolicyHash = NormalizeValue(request.LatticePolicyHash),
|
||||
InputHashes = NormalizeSortedList(request.InputHashes),
|
||||
ScoringConfigVersion = NormalizeValue(request.ScoringConfigVersion),
|
||||
EvidenceHashes = NormalizeSortedList(request.EvidenceHashes),
|
||||
AdditionalContext = NormalizeSortedDictionary(request.AdditionalContext)
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(canonical, _jsonOptions);
|
||||
}
|
||||
|
||||
private sealed class CanonicalReplayInput
|
||||
{
|
||||
public required string Version { get; init; }
|
||||
public required List<string> FeedManifests { get; init; }
|
||||
public string? RulesVersion { get; init; }
|
||||
public string? RulesHash { get; init; }
|
||||
public string? LatticePolicyVersion { get; init; }
|
||||
public string? LatticePolicyHash { get; init; }
|
||||
public required List<string> InputHashes { get; init; }
|
||||
public string? ScoringConfigVersion { get; init; }
|
||||
public required List<string> EvidenceHashes { get; init; }
|
||||
public required Dictionary<string, string> AdditionalContext { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Text;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
public sealed partial class Sha256ReplayTokenGenerator
|
||||
{
|
||||
private string ComputeHash(string input)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
return _cryptoHash.ComputeHashHex(bytes, HashAlgorithms.Sha256);
|
||||
}
|
||||
|
||||
private string ComputeTokenValue(ReplayTokenRequest request, string version)
|
||||
{
|
||||
var canonical = Canonicalize(request, version);
|
||||
return ComputeHash(canonical);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,12 @@
|
||||
|
||||
using StellaOps.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
/// <summary>
|
||||
/// Generates replay tokens using SHA-256 hashing with deterministic canonicalization.
|
||||
/// </summary>
|
||||
public sealed class Sha256ReplayTokenGenerator : IReplayTokenGenerator
|
||||
public sealed partial class Sha256ReplayTokenGenerator : IReplayTokenGenerator
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
@@ -31,7 +21,6 @@ public sealed class Sha256ReplayTokenGenerator : IReplayTokenGenerator
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var hashHex = ComputeTokenValue(request, ReplayToken.DefaultVersion);
|
||||
|
||||
return new ReplayToken(hashHex, _timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
@@ -46,7 +35,6 @@ public sealed class Sha256ReplayTokenGenerator : IReplayTokenGenerator
|
||||
}
|
||||
|
||||
var hashHex = ComputeTokenValue(request, ReplayToken.VersionWithExpiration);
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var expiresAt = now + effectiveExpiration;
|
||||
|
||||
@@ -67,14 +55,12 @@ public sealed class Sha256ReplayTokenGenerator : IReplayTokenGenerator
|
||||
ArgumentNullException.ThrowIfNull(token);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
// Check hash first
|
||||
var computed = ComputeTokenValue(request, token.Version);
|
||||
if (!string.Equals(token.Value, computed, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ReplayTokenVerificationResult.Invalid;
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if (token.IsExpired(_timeProvider.GetUtcNow()))
|
||||
{
|
||||
return ReplayTokenVerificationResult.Expired;
|
||||
@@ -82,111 +68,4 @@ public sealed class Sha256ReplayTokenGenerator : IReplayTokenGenerator
|
||||
|
||||
return ReplayTokenVerificationResult.Valid;
|
||||
}
|
||||
|
||||
private string ComputeHash(string input)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
return _cryptoHash.ComputeHashHex(bytes, HashAlgorithms.Sha256);
|
||||
}
|
||||
|
||||
private string ComputeTokenValue(ReplayTokenRequest request, string version)
|
||||
{
|
||||
var canonical = Canonicalize(request, version);
|
||||
return ComputeHash(canonical);
|
||||
}
|
||||
|
||||
private static string? NormalizeValue(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
private static List<string> NormalizeSortedList(IReadOnlyList<string>? values)
|
||||
{
|
||||
if (values is null || values.Count == 0)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
var normalized = values
|
||||
.Where(static x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(static x => x.Trim())
|
||||
.OrderBy(static x => x, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> NormalizeSortedDictionary(IReadOnlyDictionary<string, string>? values)
|
||||
{
|
||||
if (values is null || values.Count == 0)
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
var normalized = new List<KeyValuePair<string, string>>(values.Count);
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var kvp in values)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(kvp.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = kvp.Key.Trim();
|
||||
if (!seen.Add(key))
|
||||
{
|
||||
throw new ArgumentException($"AdditionalContext contains duplicate key after normalization: '{key}'.", nameof(values));
|
||||
}
|
||||
|
||||
normalized.Add(new KeyValuePair<string, string>(key, kvp.Value?.Trim() ?? string.Empty));
|
||||
}
|
||||
|
||||
var ordered = normalized
|
||||
.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal)
|
||||
.ToDictionary(static kvp => kvp.Key, static kvp => kvp.Value, StringComparer.Ordinal);
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Produces deterministic canonical representation of inputs.
|
||||
/// </summary>
|
||||
private static string Canonicalize(ReplayTokenRequest request, string version)
|
||||
{
|
||||
var canonical = new CanonicalReplayInput
|
||||
{
|
||||
Version = version,
|
||||
FeedManifests = NormalizeSortedList(request.FeedManifests),
|
||||
RulesVersion = NormalizeValue(request.RulesVersion),
|
||||
RulesHash = NormalizeValue(request.RulesHash),
|
||||
LatticePolicyVersion = NormalizeValue(request.LatticePolicyVersion),
|
||||
LatticePolicyHash = NormalizeValue(request.LatticePolicyHash),
|
||||
InputHashes = NormalizeSortedList(request.InputHashes),
|
||||
ScoringConfigVersion = NormalizeValue(request.ScoringConfigVersion),
|
||||
EvidenceHashes = NormalizeSortedList(request.EvidenceHashes),
|
||||
AdditionalContext = NormalizeSortedDictionary(request.AdditionalContext)
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(canonical, JsonOptions);
|
||||
}
|
||||
|
||||
private sealed class CanonicalReplayInput
|
||||
{
|
||||
public required string Version { get; init; }
|
||||
public required List<string> FeedManifests { get; init; }
|
||||
public string? RulesVersion { get; init; }
|
||||
public string? RulesHash { get; init; }
|
||||
public string? LatticePolicyVersion { get; init; }
|
||||
public string? LatticePolicyHash { get; init; }
|
||||
public required List<string> InputHashes { get; init; }
|
||||
public string? ScoringConfigVersion { get; init; }
|
||||
public required List<string> EvidenceHashes { get; init; }
|
||||
public required Dictionary<string, string> AdditionalContext { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Audit ReplayToken Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0043-M | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0043-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0043-A | TODO | Requires MAINT/TEST + approval. |
|
||||
| REMED-05 | DONE | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/__Libraries/StellaOps.Audit.ReplayToken/StellaOps.Audit.ReplayToken.md. File split <= 100 lines and private field naming fixed; `dotnet test src/__Tests/StellaOps.Audit.ReplayToken.Tests/StellaOps.Audit.ReplayToken.Tests.csproj` passed 2026-02-03 (60 tests). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
|
||||
Reference in New Issue
Block a user