This commit is contained in:
master
2026-02-04 19:59:20 +02:00
parent 557feefdc3
commit 5548cf83bf
1479 changed files with 53557 additions and 40339 deletions

View File

@@ -0,0 +1,23 @@
# AdvisoryAI Attestation Charter
## Mission
- Provide deterministic, offline-safe attestation models and helpers for Advisory AI workflows.
## Responsibilities
- Maintain stable attestation contracts and digest behavior.
- Keep in-memory implementations deterministic and test-friendly.
- Update local `TASKS.md` and sprint status when starting or finishing work.
## Required Reading
- `docs/README.md`
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/modules/concelier/advisory-ai-api.md`
## Working Directory & Scope
- Primary: `src/__Libraries/StellaOps.AdvisoryAI.Attestation`
- Allowed shared projects: `src/__Libraries/__Tests/StellaOps.AdvisoryAI.Attestation.Tests`
## Testing Expectations
- Add unit tests for digest and template registry behaviors.
- Keep tests deterministic and offline-friendly.

View File

@@ -0,0 +1,25 @@
namespace StellaOps.AdvisoryAI.Attestation;
/// <summary>
/// Result of creating an attestation.
/// </summary>
public sealed record AiAttestationResult
{
/// <summary>Attestation ID.</summary>
public required string AttestationId { get; init; }
/// <summary>Content digest.</summary>
public required string Digest { get; init; }
/// <summary>Whether the attestation was signed.</summary>
public bool Signed { get; init; }
/// <summary>DSSE envelope if signed.</summary>
public string? DsseEnvelope { get; init; }
/// <summary>Storage URI.</summary>
public string? StorageUri { get; init; }
/// <summary>Creation timestamp.</summary>
public required DateTimeOffset CreatedAt { get; init; }
}

View File

@@ -0,0 +1,95 @@
using Microsoft.Extensions.Logging;
using StellaOps.AdvisoryAI.Attestation.Models;
using System.Text.Json;
namespace StellaOps.AdvisoryAI.Attestation;
public sealed partial class AiAttestationService
{
/// <inheritdoc/>
public Task<AiAttestationResult> CreateRunAttestationAsync(
AiRunAttestation attestation,
bool sign = true,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
var digest = attestation.ComputeDigest();
var json = JsonSerializer.Serialize(attestation, AiAttestationJsonContext.Default.AiRunAttestation);
// In production, this would call the signer service
string? dsseEnvelope = null;
if (sign)
{
// Placeholder - real implementation would use StellaOps.Signer
dsseEnvelope = CreateMockDsseEnvelope(AiRunAttestation.PredicateType, json);
}
var stored = new StoredAttestation(
attestation.RunId,
AiRunAttestation.PredicateType,
json,
digest,
dsseEnvelope,
now);
_runAttestations[attestation.RunId] = stored;
_logger.LogInformation(
"Created run attestation {RunId} with digest {Digest}, signed={Signed}",
attestation.RunId,
digest,
sign);
return Task.FromResult(new AiAttestationResult
{
AttestationId = attestation.RunId,
Digest = digest,
Signed = sign,
DsseEnvelope = dsseEnvelope,
StorageUri = $"stella://ai-attestation/run/{attestation.RunId}",
CreatedAt = now
});
}
/// <inheritdoc/>
public Task<AiAttestationResult> CreateClaimAttestationAsync(
AiClaimAttestation attestation,
bool sign = true,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
var digest = attestation.ComputeDigest();
var json = JsonSerializer.Serialize(attestation, AiAttestationJsonContext.Default.AiClaimAttestation);
string? dsseEnvelope = null;
if (sign)
{
dsseEnvelope = CreateMockDsseEnvelope(AiClaimAttestation.PredicateType, json);
}
var stored = new StoredAttestation(
attestation.ClaimId,
AiClaimAttestation.PredicateType,
json,
digest,
dsseEnvelope,
now);
_claimAttestations[attestation.ClaimId] = stored;
_logger.LogDebug(
"Created claim attestation {ClaimId} for run {RunId}",
attestation.ClaimId,
attestation.RunId);
return Task.FromResult(new AiAttestationResult
{
AttestationId = attestation.ClaimId,
Digest = digest,
Signed = sign,
DsseEnvelope = dsseEnvelope,
StorageUri = $"stella://ai-attestation/claim/{attestation.ClaimId}",
CreatedAt = now
});
}
}

View File

@@ -0,0 +1,25 @@
namespace StellaOps.AdvisoryAI.Attestation;
public sealed partial class AiAttestationService
{
private static string CreateMockDsseEnvelope(string predicateType, string payload)
{
// Mock DSSE envelope - real implementation would use StellaOps.Signer
var payloadBase64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(payload));
return $$"""
{
"payloadType": "{{predicateType}}",
"payload": "{{payloadBase64}}",
"signatures": [{"sig": "mock-signature"}]
}
""";
}
private sealed record StoredAttestation(
string Id,
string PredicateType,
string Json,
string Digest,
string? DsseEnvelope,
DateTimeOffset CreatedAt);
}

View File

@@ -0,0 +1,55 @@
using StellaOps.AdvisoryAI.Attestation.Models;
using System.Text.Json;
namespace StellaOps.AdvisoryAI.Attestation;
public sealed partial class AiAttestationService
{
/// <inheritdoc/>
public Task<AiRunAttestation?> GetRunAttestationAsync(
string runId,
CancellationToken ct = default)
{
if (!_runAttestations.TryGetValue(runId, out var stored))
{
return Task.FromResult<AiRunAttestation?>(null);
}
var attestation = JsonSerializer.Deserialize(
stored.Json,
AiAttestationJsonContext.Default.AiRunAttestation);
return Task.FromResult(attestation);
}
/// <inheritdoc/>
public Task<IReadOnlyList<AiClaimAttestation>> GetClaimAttestationsAsync(
string runId,
CancellationToken ct = default)
{
var claims = _claimAttestations.Values
.Select(s => JsonSerializer.Deserialize(s.Json, AiAttestationJsonContext.Default.AiClaimAttestation))
.Where(c => c != null && c.RunId == runId)
.Cast<AiClaimAttestation>()
.ToList();
return Task.FromResult<IReadOnlyList<AiClaimAttestation>>(claims);
}
/// <inheritdoc/>
public Task<IReadOnlyList<AiRunAttestation>> ListRecentAttestationsAsync(
string tenantId,
int limit = 100,
CancellationToken ct = default)
{
var attestations = _runAttestations.Values
.OrderByDescending(s => s.CreatedAt)
.Select(s => JsonSerializer.Deserialize(s.Json, AiAttestationJsonContext.Default.AiRunAttestation))
.Where(a => a != null && a.TenantId == tenantId)
.Cast<AiRunAttestation>()
.Take(limit)
.ToList();
return Task.FromResult<IReadOnlyList<AiRunAttestation>>(attestations);
}
}

View File

@@ -0,0 +1,92 @@
using Microsoft.Extensions.Logging;
using StellaOps.AdvisoryAI.Attestation.Models;
using System.Text.Json;
namespace StellaOps.AdvisoryAI.Attestation;
public sealed partial class AiAttestationService
{
/// <inheritdoc/>
public Task<AiAttestationVerificationResult> VerifyRunAttestationAsync(
string runId,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
if (!_runAttestations.TryGetValue(runId, out var stored))
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
$"Run attestation {runId} not found"));
}
// Verify digest
var attestation = JsonSerializer.Deserialize(
stored.Json,
AiAttestationJsonContext.Default.AiRunAttestation);
if (attestation == null)
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
"Failed to deserialize attestation"));
}
var computedDigest = attestation.ComputeDigest();
if (computedDigest != stored.Digest)
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
"Digest mismatch",
digestValid: false));
}
// In production, verify signature via signer service
bool? signatureValid = stored.DsseEnvelope != null ? true : null;
_logger.LogDebug("Verified run attestation {RunId}", runId);
return Task.FromResult(AiAttestationVerificationResult.Success(
now,
stored.DsseEnvelope != null ? "ai-attestation-key" : null));
}
/// <inheritdoc/>
public Task<AiAttestationVerificationResult> VerifyClaimAttestationAsync(
string claimId,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
if (!_claimAttestations.TryGetValue(claimId, out var stored))
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
$"Claim attestation {claimId} not found"));
}
var attestation = JsonSerializer.Deserialize(
stored.Json,
AiAttestationJsonContext.Default.AiClaimAttestation);
if (attestation == null)
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
"Failed to deserialize attestation"));
}
var computedDigest = attestation.ComputeDigest();
if (computedDigest != stored.Digest)
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
"Digest mismatch",
digestValid: false));
}
return Task.FromResult(AiAttestationVerificationResult.Success(
now,
stored.DsseEnvelope != null ? "ai-attestation-key" : null));
}
}

View File

@@ -2,11 +2,8 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using Microsoft.Extensions.Logging;
using StellaOps.AdvisoryAI.Attestation.Models;
using System.Collections.Concurrent;
using System.Text.Json;
namespace StellaOps.AdvisoryAI.Attestation;
@@ -18,7 +15,7 @@ namespace StellaOps.AdvisoryAI.Attestation;
/// This implementation stores attestations in memory. For production,
/// use a database-backed implementation with signing integration.
/// </remarks>
public sealed class AiAttestationService : IAiAttestationService
public sealed partial class AiAttestationService : IAiAttestationService
{
private readonly TimeProvider _timeProvider;
private readonly ILogger<AiAttestationService> _logger;
@@ -32,244 +29,4 @@ public sealed class AiAttestationService : IAiAttestationService
_timeProvider = timeProvider;
_logger = logger;
}
/// <inheritdoc/>
public Task<AiAttestationResult> CreateRunAttestationAsync(
AiRunAttestation attestation,
bool sign = true,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
var digest = attestation.ComputeDigest();
var json = JsonSerializer.Serialize(attestation, AiAttestationJsonContext.Default.AiRunAttestation);
// In production, this would call the signer service
string? dsseEnvelope = null;
if (sign)
{
// Placeholder - real implementation would use StellaOps.Signer
dsseEnvelope = CreateMockDsseEnvelope(AiRunAttestation.PredicateType, json);
}
var stored = new StoredAttestation(
attestation.RunId,
AiRunAttestation.PredicateType,
json,
digest,
dsseEnvelope,
now);
_runAttestations[attestation.RunId] = stored;
_logger.LogInformation(
"Created run attestation {RunId} with digest {Digest}, signed={Signed}",
attestation.RunId,
digest,
sign);
return Task.FromResult(new AiAttestationResult
{
AttestationId = attestation.RunId,
Digest = digest,
Signed = sign,
DsseEnvelope = dsseEnvelope,
StorageUri = $"stella://ai-attestation/run/{attestation.RunId}",
CreatedAt = now
});
}
/// <inheritdoc/>
public Task<AiAttestationResult> CreateClaimAttestationAsync(
AiClaimAttestation attestation,
bool sign = true,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
var digest = attestation.ComputeDigest();
var json = JsonSerializer.Serialize(attestation, AiAttestationJsonContext.Default.AiClaimAttestation);
string? dsseEnvelope = null;
if (sign)
{
dsseEnvelope = CreateMockDsseEnvelope(AiClaimAttestation.PredicateType, json);
}
var stored = new StoredAttestation(
attestation.ClaimId,
AiClaimAttestation.PredicateType,
json,
digest,
dsseEnvelope,
now);
_claimAttestations[attestation.ClaimId] = stored;
_logger.LogDebug(
"Created claim attestation {ClaimId} for run {RunId}",
attestation.ClaimId,
attestation.RunId);
return Task.FromResult(new AiAttestationResult
{
AttestationId = attestation.ClaimId,
Digest = digest,
Signed = sign,
DsseEnvelope = dsseEnvelope,
StorageUri = $"stella://ai-attestation/claim/{attestation.ClaimId}",
CreatedAt = now
});
}
/// <inheritdoc/>
public Task<AiAttestationVerificationResult> VerifyRunAttestationAsync(
string runId,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
if (!_runAttestations.TryGetValue(runId, out var stored))
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
$"Run attestation {runId} not found"));
}
// Verify digest
var attestation = JsonSerializer.Deserialize(
stored.Json,
AiAttestationJsonContext.Default.AiRunAttestation);
if (attestation == null)
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
"Failed to deserialize attestation"));
}
var computedDigest = attestation.ComputeDigest();
if (computedDigest != stored.Digest)
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
"Digest mismatch",
digestValid: false));
}
// In production, verify signature via signer service
bool? signatureValid = stored.DsseEnvelope != null ? true : null;
_logger.LogDebug("Verified run attestation {RunId}", runId);
return Task.FromResult(AiAttestationVerificationResult.Success(
now,
stored.DsseEnvelope != null ? "ai-attestation-key" : null));
}
/// <inheritdoc/>
public Task<AiAttestationVerificationResult> VerifyClaimAttestationAsync(
string claimId,
CancellationToken ct = default)
{
var now = _timeProvider.GetUtcNow();
if (!_claimAttestations.TryGetValue(claimId, out var stored))
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
$"Claim attestation {claimId} not found"));
}
var attestation = JsonSerializer.Deserialize(
stored.Json,
AiAttestationJsonContext.Default.AiClaimAttestation);
if (attestation == null)
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
"Failed to deserialize attestation"));
}
var computedDigest = attestation.ComputeDigest();
if (computedDigest != stored.Digest)
{
return Task.FromResult(AiAttestationVerificationResult.Failure(
now,
"Digest mismatch",
digestValid: false));
}
return Task.FromResult(AiAttestationVerificationResult.Success(
now,
stored.DsseEnvelope != null ? "ai-attestation-key" : null));
}
/// <inheritdoc/>
public Task<AiRunAttestation?> GetRunAttestationAsync(
string runId,
CancellationToken ct = default)
{
if (!_runAttestations.TryGetValue(runId, out var stored))
{
return Task.FromResult<AiRunAttestation?>(null);
}
var attestation = JsonSerializer.Deserialize(
stored.Json,
AiAttestationJsonContext.Default.AiRunAttestation);
return Task.FromResult(attestation);
}
/// <inheritdoc/>
public Task<IReadOnlyList<AiClaimAttestation>> GetClaimAttestationsAsync(
string runId,
CancellationToken ct = default)
{
var claims = _claimAttestations.Values
.Select(s => JsonSerializer.Deserialize(s.Json, AiAttestationJsonContext.Default.AiClaimAttestation))
.Where(c => c != null && c.RunId == runId)
.Cast<AiClaimAttestation>()
.ToList();
return Task.FromResult<IReadOnlyList<AiClaimAttestation>>(claims);
}
/// <inheritdoc/>
public Task<IReadOnlyList<AiRunAttestation>> ListRecentAttestationsAsync(
string tenantId,
int limit = 100,
CancellationToken ct = default)
{
var attestations = _runAttestations.Values
.OrderByDescending(s => s.CreatedAt)
.Select(s => JsonSerializer.Deserialize(s.Json, AiAttestationJsonContext.Default.AiRunAttestation))
.Where(a => a != null && a.TenantId == tenantId)
.Cast<AiRunAttestation>()
.Take(limit)
.ToList();
return Task.FromResult<IReadOnlyList<AiRunAttestation>>(attestations);
}
private static string CreateMockDsseEnvelope(string predicateType, string payload)
{
// Mock DSSE envelope - real implementation would use StellaOps.Signer
var payloadBase64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(payload));
return $$"""
{
"payloadType": "{{predicateType}}",
"payload": "{{payloadBase64}}",
"signatures": [{"sig": "mock-signature"}]
}
""";
}
private sealed record StoredAttestation(
string Id,
string PredicateType,
string Json,
string Digest,
string? DsseEnvelope,
DateTimeOffset CreatedAt);
}

View File

@@ -0,0 +1,60 @@
namespace StellaOps.AdvisoryAI.Attestation;
/// <summary>
/// Result of verifying an attestation.
/// </summary>
public sealed record AiAttestationVerificationResult
{
/// <summary>Whether verification succeeded.</summary>
public required bool Valid { get; init; }
/// <summary>Verification timestamp.</summary>
public required DateTimeOffset VerifiedAt { get; init; }
/// <summary>Signing key ID if signed.</summary>
public string? SigningKeyId { get; init; }
/// <summary>Key expiration if applicable.</summary>
public DateTimeOffset? KeyExpiresAt { get; init; }
/// <summary>Digest verification result.</summary>
public bool DigestValid { get; init; }
/// <summary>Signature verification result.</summary>
public bool? SignatureValid { get; init; }
/// <summary>Verification failure reason if invalid.</summary>
public string? FailureReason { get; init; }
/// <summary>
/// Creates a successful verification result.
/// </summary>
public static AiAttestationVerificationResult Success(
DateTimeOffset verifiedAt,
string? signingKeyId = null,
DateTimeOffset? keyExpiresAt = null) => new()
{
Valid = true,
VerifiedAt = verifiedAt,
SigningKeyId = signingKeyId,
KeyExpiresAt = keyExpiresAt,
DigestValid = true,
SignatureValid = signingKeyId != null ? true : null
};
/// <summary>
/// Creates a failed verification result.
/// </summary>
public static AiAttestationVerificationResult Failure(
DateTimeOffset verifiedAt,
string reason,
bool digestValid = false,
bool? signatureValid = null) => new()
{
Valid = false,
VerifiedAt = verifiedAt,
DigestValid = digestValid,
SignatureValid = signatureValid,
FailureReason = reason
};
}

View File

@@ -88,86 +88,3 @@ public interface IAiAttestationService
int limit = 100,
CancellationToken ct = default);
}
/// <summary>
/// Result of creating an attestation.
/// </summary>
public sealed record AiAttestationResult
{
/// <summary>Attestation ID.</summary>
public required string AttestationId { get; init; }
/// <summary>Content digest.</summary>
public required string Digest { get; init; }
/// <summary>Whether the attestation was signed.</summary>
public bool Signed { get; init; }
/// <summary>DSSE envelope if signed.</summary>
public string? DsseEnvelope { get; init; }
/// <summary>Storage URI.</summary>
public string? StorageUri { get; init; }
/// <summary>Creation timestamp.</summary>
public required DateTimeOffset CreatedAt { get; init; }
}
/// <summary>
/// Result of verifying an attestation.
/// </summary>
public sealed record AiAttestationVerificationResult
{
/// <summary>Whether verification succeeded.</summary>
public required bool Valid { get; init; }
/// <summary>Verification timestamp.</summary>
public required DateTimeOffset VerifiedAt { get; init; }
/// <summary>Signing key ID if signed.</summary>
public string? SigningKeyId { get; init; }
/// <summary>Key expiration if applicable.</summary>
public DateTimeOffset? KeyExpiresAt { get; init; }
/// <summary>Digest verification result.</summary>
public bool DigestValid { get; init; }
/// <summary>Signature verification result.</summary>
public bool? SignatureValid { get; init; }
/// <summary>Verification failure reason if invalid.</summary>
public string? FailureReason { get; init; }
/// <summary>
/// Creates a successful verification result.
/// </summary>
public static AiAttestationVerificationResult Success(
DateTimeOffset verifiedAt,
string? signingKeyId = null,
DateTimeOffset? keyExpiresAt = null) => new()
{
Valid = true,
VerifiedAt = verifiedAt,
SigningKeyId = signingKeyId,
KeyExpiresAt = keyExpiresAt,
DigestValid = true,
SignatureValid = signingKeyId != null ? true : null
};
/// <summary>
/// Creates a failed verification result.
/// </summary>
public static AiAttestationVerificationResult Failure(
DateTimeOffset verifiedAt,
string reason,
bool digestValid = false,
bool? signatureValid = null) => new()
{
Valid = false,
VerifiedAt = verifiedAt,
DigestValid = digestValid,
SignatureValid = signatureValid,
FailureReason = reason
};
}

View File

@@ -0,0 +1,51 @@
// <copyright file="IPromptTemplateRegistry.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using StellaOps.AdvisoryAI.Attestation.Models;
namespace StellaOps.AdvisoryAI.Attestation;
/// <summary>
/// Interface for prompt template registry.
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-004
/// </summary>
public interface IPromptTemplateRegistry
{
/// <summary>
/// Registers a prompt template with version.
/// </summary>
/// <param name="name">Template name.</param>
/// <param name="version">Template version.</param>
/// <param name="template">Template content.</param>
void Register(string name, string version, string template);
/// <summary>
/// Gets template info including hash.
/// </summary>
/// <param name="name">Template name.</param>
/// <returns>Template info or null if not found.</returns>
PromptTemplateInfo? GetTemplateInfo(string name);
/// <summary>
/// Gets template info for a specific version.
/// </summary>
/// <param name="name">Template name.</param>
/// <param name="version">Template version.</param>
/// <returns>Template info or null if not found.</returns>
PromptTemplateInfo? GetTemplateInfo(string name, string version);
/// <summary>
/// Verifies a template hash matches registered version.
/// </summary>
/// <param name="name">Template name.</param>
/// <param name="expectedHash">Expected hash.</param>
/// <returns>True if hash matches.</returns>
bool VerifyHash(string name, string expectedHash);
/// <summary>
/// Gets all registered templates.
/// </summary>
/// <returns>All template info records.</returns>
IReadOnlyList<PromptTemplateInfo> GetAllTemplates();
}

View File

@@ -0,0 +1,18 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.AdvisoryAI.Attestation.Models;
public sealed partial record AiClaimAttestation
{
/// <summary>
/// Computes the content digest for this attestation.
/// </summary>
public string ComputeDigest()
{
var json = JsonSerializer.Serialize(this, AiAttestationJsonContext.Default.AiClaimAttestation);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}

View File

@@ -0,0 +1,48 @@
using System.Security.Cryptography;
using System.Text;
namespace StellaOps.AdvisoryAI.Attestation.Models;
public sealed partial record AiClaimAttestation
{
/// <summary>
/// Creates a claim attestation from a claim evidence.
/// </summary>
public static AiClaimAttestation FromClaimEvidence(
ClaimEvidence evidence,
string runId,
string turnId,
string tenantId,
DateTimeOffset timestamp,
AiRunContext? context = null)
{
var claimDigest = ComputeClaimDigest(evidence.Text);
var claimId = $"claim-{Guid.NewGuid():N}";
var attestation = new AiClaimAttestation
{
ClaimId = claimId,
RunId = runId,
TurnId = turnId,
TenantId = tenantId,
ClaimText = evidence.Text,
ClaimDigest = claimDigest,
Category = evidence.Category,
GroundedBy = evidence.GroundedBy,
GroundingScore = evidence.GroundingScore,
Verified = evidence.Verified,
Timestamp = timestamp,
Context = context,
ContentDigest = "" // Placeholder, computed below
};
// Now compute the actual content digest
return attestation with { ContentDigest = attestation.ComputeDigest() };
}
private static string ComputeClaimDigest(string claimText)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(claimText));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}

View File

@@ -3,9 +3,6 @@
// </copyright>
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.AdvisoryAI.Attestation.Models;
@@ -21,7 +18,7 @@ namespace StellaOps.AdvisoryAI.Attestation.Models;
/// - Claim-specific evidence linkage
/// - Selective claim citation in reports
/// </remarks>
public sealed record AiClaimAttestation
public sealed partial record AiClaimAttestation
{
/// <summary>Attestation type URI.</summary>
public const string PredicateType = "https://stellaops.org/attestation/ai-claim/v1";
@@ -86,54 +83,4 @@ public sealed record AiClaimAttestation
[JsonPropertyName("claimType")]
public string? ClaimType { get; init; }
/// <summary>
/// Computes the content digest for this attestation.
/// </summary>
public string ComputeDigest()
{
var json = JsonSerializer.Serialize(this, AiAttestationJsonContext.Default.AiClaimAttestation);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
/// <summary>
/// Creates a claim attestation from a claim evidence.
/// </summary>
public static AiClaimAttestation FromClaimEvidence(
ClaimEvidence evidence,
string runId,
string turnId,
string tenantId,
DateTimeOffset timestamp,
AiRunContext? context = null)
{
var claimDigest = ComputeClaimDigest(evidence.Text);
var claimId = $"claim-{Guid.NewGuid():N}";
var attestation = new AiClaimAttestation
{
ClaimId = claimId,
RunId = runId,
TurnId = turnId,
TenantId = tenantId,
ClaimText = evidence.Text,
ClaimDigest = claimDigest,
Category = evidence.Category,
GroundedBy = evidence.GroundedBy,
GroundingScore = evidence.GroundingScore,
Verified = evidence.Verified,
Timestamp = timestamp,
Context = context,
ContentDigest = "" // Placeholder, computed below
};
// Now compute the actual content digest
return attestation with { ContentDigest = attestation.ComputeDigest() };
}
private static string ComputeClaimDigest(string claimText)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(claimText));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}

View File

@@ -0,0 +1,18 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.AdvisoryAI.Attestation.Models;
public sealed partial record AiRunAttestation
{
/// <summary>
/// Computes the content digest for this attestation.
/// </summary>
public string ComputeDigest()
{
var json = JsonSerializer.Serialize(this, AiAttestationJsonContext.Default.AiRunAttestation);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}

View File

@@ -3,9 +3,6 @@
// </copyright>
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.AdvisoryAI.Attestation.Models;
@@ -23,7 +20,7 @@ namespace StellaOps.AdvisoryAI.Attestation.Models;
/// - What was said (content digests)
/// - What claims were made and their grounding evidence
/// </remarks>
public sealed record AiRunAttestation
public sealed partial record AiRunAttestation
{
/// <summary>Attestation type URI.</summary>
public const string PredicateType = "https://stellaops.org/attestation/ai-run/v1";
@@ -84,35 +81,4 @@ public sealed record AiRunAttestation
[JsonPropertyName("errorMessage")]
public string? ErrorMessage { get; init; }
/// <summary>
/// Computes the content digest for this attestation.
/// </summary>
public string ComputeDigest()
{
var json = JsonSerializer.Serialize(this, AiAttestationJsonContext.Default.AiRunAttestation);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(json));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}
/// <summary>
/// AI run status.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<AiRunStatus>))]
public enum AiRunStatus
{
/// <summary>Run completed successfully.</summary>
Completed,
/// <summary>Run failed.</summary>
Failed,
/// <summary>Run was cancelled.</summary>
Cancelled,
/// <summary>Run timed out.</summary>
TimedOut,
/// <summary>Run was blocked by guardrails.</summary>
Blocked
}

View File

@@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
namespace StellaOps.AdvisoryAI.Attestation.Models;
/// <summary>
/// AI run status.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<AiRunStatus>))]
public enum AiRunStatus
{
/// <summary>Run completed successfully.</summary>
Completed,
/// <summary>Run failed.</summary>
Failed,
/// <summary>Run was cancelled.</summary>
Cancelled,
/// <summary>Run timed out.</summary>
TimedOut,
/// <summary>Run was blocked by guardrails.</summary>
Blocked
}

View File

@@ -0,0 +1,13 @@
using System.Security.Cryptography;
using System.Text;
namespace StellaOps.AdvisoryAI.Attestation;
public sealed partial class PromptTemplateRegistry
{
private static string ComputeDigest(string content)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}

View File

@@ -0,0 +1,48 @@
using Microsoft.Extensions.Logging;
using StellaOps.AdvisoryAI.Attestation.Models;
namespace StellaOps.AdvisoryAI.Attestation;
public sealed partial class PromptTemplateRegistry
{
/// <inheritdoc/>
public PromptTemplateInfo? GetTemplateInfo(string name)
{
return _latestVersions.TryGetValue(name, out var info) ? info : null;
}
/// <inheritdoc/>
public PromptTemplateInfo? GetTemplateInfo(string name, string version)
{
return _allVersions.TryGetValue((name, version), out var info) ? info : null;
}
/// <inheritdoc/>
public bool VerifyHash(string name, string expectedHash)
{
if (!_latestVersions.TryGetValue(name, out var info))
{
_logger.LogWarning("Template {Name} not found for hash verification", name);
return false;
}
var matches = string.Equals(info.Digest, expectedHash, StringComparison.OrdinalIgnoreCase);
if (!matches)
{
_logger.LogWarning(
"Hash mismatch for template {Name}: expected {Expected}, got {Actual}",
name,
expectedHash,
info.Digest);
}
return matches;
}
/// <inheritdoc/>
public IReadOnlyList<PromptTemplateInfo> GetAllTemplates()
{
return [.. _latestVersions.Values];
}
}

View File

@@ -2,65 +2,17 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using Microsoft.Extensions.Logging;
using StellaOps.AdvisoryAI.Attestation.Models;
using System.Collections.Concurrent;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace StellaOps.AdvisoryAI.Attestation;
/// <summary>
/// Interface for prompt template registry.
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-004
/// </summary>
public interface IPromptTemplateRegistry
{
/// <summary>
/// Registers a prompt template with version.
/// </summary>
/// <param name="name">Template name.</param>
/// <param name="version">Template version.</param>
/// <param name="template">Template content.</param>
void Register(string name, string version, string template);
/// <summary>
/// Gets template info including hash.
/// </summary>
/// <param name="name">Template name.</param>
/// <returns>Template info or null if not found.</returns>
PromptTemplateInfo? GetTemplateInfo(string name);
/// <summary>
/// Gets template info for a specific version.
/// </summary>
/// <param name="name">Template name.</param>
/// <param name="version">Template version.</param>
/// <returns>Template info or null if not found.</returns>
PromptTemplateInfo? GetTemplateInfo(string name, string version);
/// <summary>
/// Verifies a template hash matches registered version.
/// </summary>
/// <param name="name">Template name.</param>
/// <param name="expectedHash">Expected hash.</param>
/// <returns>True if hash matches.</returns>
bool VerifyHash(string name, string expectedHash);
/// <summary>
/// Gets all registered templates.
/// </summary>
/// <returns>All template info records.</returns>
IReadOnlyList<PromptTemplateInfo> GetAllTemplates();
}
/// <summary>
/// In-memory implementation of prompt template registry.
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-004
/// </summary>
public sealed class PromptTemplateRegistry : IPromptTemplateRegistry
public sealed partial class PromptTemplateRegistry : IPromptTemplateRegistry
{
private readonly TimeProvider _timeProvider;
private readonly ILogger<PromptTemplateRegistry> _logger;
@@ -101,51 +53,4 @@ public sealed class PromptTemplateRegistry : IPromptTemplateRegistry
version,
digest);
}
/// <inheritdoc/>
public PromptTemplateInfo? GetTemplateInfo(string name)
{
return _latestVersions.TryGetValue(name, out var info) ? info : null;
}
/// <inheritdoc/>
public PromptTemplateInfo? GetTemplateInfo(string name, string version)
{
return _allVersions.TryGetValue((name, version), out var info) ? info : null;
}
/// <inheritdoc/>
public bool VerifyHash(string name, string expectedHash)
{
if (!_latestVersions.TryGetValue(name, out var info))
{
_logger.LogWarning("Template {Name} not found for hash verification", name);
return false;
}
var matches = string.Equals(info.Digest, expectedHash, StringComparison.OrdinalIgnoreCase);
if (!matches)
{
_logger.LogWarning(
"Hash mismatch for template {Name}: expected {Expected}, got {Actual}",
name,
expectedHash,
info.Digest);
}
return matches;
}
/// <inheritdoc/>
public IReadOnlyList<PromptTemplateInfo> GetAllTemplates()
{
return [.. _latestVersions.Values];
}
private static string ComputeDigest(string content)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}

View File

@@ -0,0 +1,73 @@
using StellaOps.AdvisoryAI.Attestation.Models;
using System.Collections.Immutable;
namespace StellaOps.AdvisoryAI.Attestation.Storage;
public partial interface IAiAttestationStore
{
/// <summary>
/// Get a run attestation by run ID.
/// </summary>
/// <param name="runId">The run ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The attestation or null if not found.</returns>
Task<AiRunAttestation?> GetRunAttestationAsync(string runId, CancellationToken ct);
/// <summary>
/// Get all claim attestations for a run.
/// </summary>
/// <param name="runId">The run ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of claim attestations.</returns>
Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsAsync(string runId, CancellationToken ct);
/// <summary>
/// Get claim attestations for a specific turn.
/// </summary>
/// <param name="runId">The run ID.</param>
/// <param name="turnId">The turn ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of claim attestations for the turn.</returns>
Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsByTurnAsync(
string runId,
string turnId,
CancellationToken ct);
/// <summary>
/// Get the signed envelope for a run.
/// </summary>
/// <param name="runId">The run ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The signed envelope or null if not found.</returns>
Task<object?> GetSignedEnvelopeAsync(string runId, CancellationToken ct);
/// <summary>
/// Check if a run attestation exists.
/// </summary>
/// <param name="runId">The run ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>True if the attestation exists.</returns>
Task<bool> ExistsAsync(string runId, CancellationToken ct);
/// <summary>
/// Get attestations by tenant within a time range.
/// </summary>
/// <param name="tenantId">The tenant ID.</param>
/// <param name="from">Start time.</param>
/// <param name="to">End time.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of run attestations.</returns>
Task<ImmutableArray<AiRunAttestation>> GetByTenantAsync(
string tenantId,
DateTimeOffset from,
DateTimeOffset to,
CancellationToken ct);
/// <summary>
/// Get attestation by content digest.
/// </summary>
/// <param name="contentDigest">The content digest.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The claim attestation or null if not found.</returns>
Task<AiClaimAttestation?> GetByContentDigestAsync(string contentDigest, CancellationToken ct);
}

View File

@@ -12,7 +12,7 @@ namespace StellaOps.AdvisoryAI.Attestation.Storage;
/// Interface for storing and retrieving AI attestations.
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-006
/// </summary>
public interface IAiAttestationStore
public partial interface IAiAttestationStore
{
/// <summary>
/// Store a run attestation.
@@ -35,70 +35,4 @@ public interface IAiAttestationStore
/// <param name="attestation">The attestation to store.</param>
/// <param name="ct">Cancellation token.</param>
Task StoreClaimAttestationAsync(AiClaimAttestation attestation, CancellationToken ct);
/// <summary>
/// Get a run attestation by run ID.
/// </summary>
/// <param name="runId">The run ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The attestation or null if not found.</returns>
Task<AiRunAttestation?> GetRunAttestationAsync(string runId, CancellationToken ct);
/// <summary>
/// Get all claim attestations for a run.
/// </summary>
/// <param name="runId">The run ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of claim attestations.</returns>
Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsAsync(string runId, CancellationToken ct);
/// <summary>
/// Get claim attestations for a specific turn.
/// </summary>
/// <param name="runId">The run ID.</param>
/// <param name="turnId">The turn ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of claim attestations for the turn.</returns>
Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsByTurnAsync(
string runId,
string turnId,
CancellationToken ct);
/// <summary>
/// Get the signed envelope for a run.
/// </summary>
/// <param name="runId">The run ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The signed envelope or null if not found.</returns>
Task<object?> GetSignedEnvelopeAsync(string runId, CancellationToken ct);
/// <summary>
/// Check if a run attestation exists.
/// </summary>
/// <param name="runId">The run ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>True if the attestation exists.</returns>
Task<bool> ExistsAsync(string runId, CancellationToken ct);
/// <summary>
/// Get attestations by tenant within a time range.
/// </summary>
/// <param name="tenantId">The tenant ID.</param>
/// <param name="from">Start time.</param>
/// <param name="to">End time.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of run attestations.</returns>
Task<ImmutableArray<AiRunAttestation>> GetByTenantAsync(
string tenantId,
DateTimeOffset from,
DateTimeOffset to,
CancellationToken ct);
/// <summary>
/// Get attestation by content digest.
/// </summary>
/// <param name="contentDigest">The content digest.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The claim attestation or null if not found.</returns>
Task<AiClaimAttestation?> GetByContentDigestAsync(string contentDigest, CancellationToken ct);
}

View File

@@ -0,0 +1,28 @@
namespace StellaOps.AdvisoryAI.Attestation.Storage;
public sealed partial class InMemoryAiAttestationStore
{
/// <summary>
/// Clear all stored attestations. Useful for testing.
/// </summary>
public void Clear()
{
_runAttestations.Clear();
_signedEnvelopes.Clear();
_claimAttestations.Clear();
_digestIndex.Clear();
}
/// <summary>
/// Get count of run attestations. Useful for testing.
/// </summary>
public int RunAttestationCount => _runAttestations.Count;
/// <summary>
/// Get count of all claim attestations. Useful for testing.
/// </summary>
public int ClaimAttestationCount => _claimAttestations.Values.Sum(c =>
{
lock (c) { return c.Count; }
});
}

View File

@@ -0,0 +1,85 @@
using StellaOps.AdvisoryAI.Attestation.Models;
using System.Collections.Immutable;
namespace StellaOps.AdvisoryAI.Attestation.Storage;
public sealed partial class InMemoryAiAttestationStore
{
/// <inheritdoc/>
public Task<AiRunAttestation?> GetRunAttestationAsync(string runId, CancellationToken ct)
{
_runAttestations.TryGetValue(runId, out var attestation);
return Task.FromResult(attestation);
}
/// <inheritdoc/>
public Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsAsync(string runId, CancellationToken ct)
{
if (_claimAttestations.TryGetValue(runId, out var claims))
{
lock (claims)
{
return Task.FromResult(claims.ToImmutableArray());
}
}
return Task.FromResult(ImmutableArray<AiClaimAttestation>.Empty);
}
/// <inheritdoc/>
public Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsByTurnAsync(
string runId,
string turnId,
CancellationToken ct)
{
if (_claimAttestations.TryGetValue(runId, out var claims))
{
lock (claims)
{
var filtered = claims
.Where(c => c.TurnId == turnId)
.ToImmutableArray();
return Task.FromResult(filtered);
}
}
return Task.FromResult(ImmutableArray<AiClaimAttestation>.Empty);
}
/// <inheritdoc/>
public Task<object?> GetSignedEnvelopeAsync(string runId, CancellationToken ct)
{
_signedEnvelopes.TryGetValue(runId, out var envelope);
return Task.FromResult(envelope);
}
/// <inheritdoc/>
public Task<bool> ExistsAsync(string runId, CancellationToken ct)
{
return Task.FromResult(_runAttestations.ContainsKey(runId));
}
/// <inheritdoc/>
public Task<ImmutableArray<AiRunAttestation>> GetByTenantAsync(
string tenantId,
DateTimeOffset from,
DateTimeOffset to,
CancellationToken ct)
{
var results = _runAttestations.Values
.Where(a => a.TenantId == tenantId &&
a.StartedAt >= from &&
a.StartedAt <= to)
.OrderBy(a => a.StartedAt)
.ToImmutableArray();
return Task.FromResult(results);
}
/// <inheritdoc/>
public Task<AiClaimAttestation?> GetByContentDigestAsync(string contentDigest, CancellationToken ct)
{
_digestIndex.TryGetValue(contentDigest, out var attestation);
return Task.FromResult(attestation);
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.Extensions.Logging;
using StellaOps.AdvisoryAI.Attestation.Models;
namespace StellaOps.AdvisoryAI.Attestation.Storage;
public sealed partial class InMemoryAiAttestationStore
{
/// <inheritdoc/>
public Task StoreRunAttestationAsync(AiRunAttestation attestation, CancellationToken ct)
{
_runAttestations[attestation.RunId] = attestation;
_logger.LogDebug("Stored run attestation for RunId {RunId}", attestation.RunId);
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task StoreSignedEnvelopeAsync(string runId, object envelope, CancellationToken ct)
{
_signedEnvelopes[runId] = envelope;
_logger.LogDebug("Stored signed envelope for RunId {RunId}", runId);
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task StoreClaimAttestationAsync(AiClaimAttestation attestation, CancellationToken ct)
{
var claims = _claimAttestations.GetOrAdd(attestation.RunId, _ => []);
lock (claims)
{
claims.Add(attestation);
}
_digestIndex[attestation.ContentDigest] = attestation;
_logger.LogDebug(
"Stored claim attestation for RunId {RunId}, TurnId {TurnId}",
attestation.RunId,
attestation.TurnId);
return Task.CompletedTask;
}
}

View File

@@ -2,11 +2,9 @@
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using Microsoft.Extensions.Logging;
using StellaOps.AdvisoryAI.Attestation.Models;
using System.Collections.Concurrent;
using System.Collections.Immutable;
namespace StellaOps.AdvisoryAI.Attestation.Storage;
@@ -15,7 +13,7 @@ namespace StellaOps.AdvisoryAI.Attestation.Storage;
/// Useful for testing and development.
/// Sprint: SPRINT_20260109_011_001 Task: AIAT-006
/// </summary>
public sealed class InMemoryAiAttestationStore : IAiAttestationStore
public sealed partial class InMemoryAiAttestationStore : IAiAttestationStore
{
private readonly ConcurrentDictionary<string, AiRunAttestation> _runAttestations = new();
private readonly ConcurrentDictionary<string, object> _signedEnvelopes = new();
@@ -27,141 +25,4 @@ public sealed class InMemoryAiAttestationStore : IAiAttestationStore
{
_logger = logger;
}
/// <inheritdoc/>
public Task StoreRunAttestationAsync(AiRunAttestation attestation, CancellationToken ct)
{
_runAttestations[attestation.RunId] = attestation;
_logger.LogDebug("Stored run attestation for RunId {RunId}", attestation.RunId);
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task StoreSignedEnvelopeAsync(string runId, object envelope, CancellationToken ct)
{
_signedEnvelopes[runId] = envelope;
_logger.LogDebug("Stored signed envelope for RunId {RunId}", runId);
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task StoreClaimAttestationAsync(AiClaimAttestation attestation, CancellationToken ct)
{
var claims = _claimAttestations.GetOrAdd(attestation.RunId, _ => []);
lock (claims)
{
claims.Add(attestation);
}
_digestIndex[attestation.ContentDigest] = attestation;
_logger.LogDebug(
"Stored claim attestation for RunId {RunId}, TurnId {TurnId}",
attestation.RunId,
attestation.TurnId);
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task<AiRunAttestation?> GetRunAttestationAsync(string runId, CancellationToken ct)
{
_runAttestations.TryGetValue(runId, out var attestation);
return Task.FromResult(attestation);
}
/// <inheritdoc/>
public Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsAsync(string runId, CancellationToken ct)
{
if (_claimAttestations.TryGetValue(runId, out var claims))
{
lock (claims)
{
return Task.FromResult(claims.ToImmutableArray());
}
}
return Task.FromResult(ImmutableArray<AiClaimAttestation>.Empty);
}
/// <inheritdoc/>
public Task<ImmutableArray<AiClaimAttestation>> GetClaimAttestationsByTurnAsync(
string runId,
string turnId,
CancellationToken ct)
{
if (_claimAttestations.TryGetValue(runId, out var claims))
{
lock (claims)
{
var filtered = claims
.Where(c => c.TurnId == turnId)
.ToImmutableArray();
return Task.FromResult(filtered);
}
}
return Task.FromResult(ImmutableArray<AiClaimAttestation>.Empty);
}
/// <inheritdoc/>
public Task<object?> GetSignedEnvelopeAsync(string runId, CancellationToken ct)
{
_signedEnvelopes.TryGetValue(runId, out var envelope);
return Task.FromResult(envelope);
}
/// <inheritdoc/>
public Task<bool> ExistsAsync(string runId, CancellationToken ct)
{
return Task.FromResult(_runAttestations.ContainsKey(runId));
}
/// <inheritdoc/>
public Task<ImmutableArray<AiRunAttestation>> GetByTenantAsync(
string tenantId,
DateTimeOffset from,
DateTimeOffset to,
CancellationToken ct)
{
var results = _runAttestations.Values
.Where(a => a.TenantId == tenantId &&
a.StartedAt >= from &&
a.StartedAt <= to)
.OrderBy(a => a.StartedAt)
.ToImmutableArray();
return Task.FromResult(results);
}
/// <inheritdoc/>
public Task<AiClaimAttestation?> GetByContentDigestAsync(string contentDigest, CancellationToken ct)
{
_digestIndex.TryGetValue(contentDigest, out var attestation);
return Task.FromResult(attestation);
}
/// <summary>
/// Clear all stored attestations. Useful for testing.
/// </summary>
public void Clear()
{
_runAttestations.Clear();
_signedEnvelopes.Clear();
_claimAttestations.Clear();
_digestIndex.Clear();
}
/// <summary>
/// Get count of run attestations. Useful for testing.
/// </summary>
public int RunAttestationCount => _runAttestations.Count;
/// <summary>
/// Get count of all claim attestations. Useful for testing.
/// </summary>
public int ClaimAttestationCount => _claimAttestations.Values.Sum(c =>
{
lock (c) { return c.Count; }
});
}

View File

@@ -4,5 +4,5 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/__Libraries/StellaOps.AdvisoryAI.Attestation/StellaOps.AdvisoryAI.Attestation.md. |
| REMED-05 | DONE | Split service/registry/models/store into <=100-line partials; `dotnet test src/__Libraries/__Tests/StellaOps.AdvisoryAI.Attestation.Tests/StellaOps.AdvisoryAI.Attestation.Tests.csproj -p:BuildInParallel=false -p:UseSharedCompilation=false` passed (58 tests) 2026-02-04. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |