< Summary

Information
Class: AsiBackbone.Core.HostIntegration.GovernedOperationExecutionReceipt
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/HostIntegration/GovernedOperationExecutionReceipt.cs
Line coverage
97%
Covered lines: 113
Uncovered lines: 3
Coverable lines: 116
Total lines: 257
Line coverage: 97.4%
Branch coverage
84%
Covered branches: 37
Total branches: 44
Branch coverage: 84%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/HostIntegration/GovernedOperationExecutionReceipt.cs

#LineLine coverage
 1using System.Collections.ObjectModel;
 2using System.Globalization;
 3
 4namespace AsiBackbone.Core.HostIntegration;
 5
 6/// <summary>
 7/// Represents a framework-neutral completion receipt for one governed host execution attempt.
 8/// </summary>
 9/// <remarks>
 10/// The receipt binds governance lifecycle evidence to host-owned persistence without copying raw
 11/// original or current application values into AsiBackbone. Mutation details remain authoritative
 12/// in the host audit store; this receipt carries only opaque identifiers, counts, hashes, and
 13/// minimized metadata.
 14/// </remarks>
 15public sealed class GovernedOperationExecutionReceipt
 16{
 17    private const int MaximumIdentifierLength = 256;
 18    private const int MaximumProviderLength = 256;
 19    private const int MaximumHashLength = 1024;
 20    private const int MaximumAlgorithmLength = 128;
 21    private const int MaximumMetadataKeyLength = 128;
 22    private const int MaximumMetadataValueLength = 2048;
 23
 424    private static readonly IReadOnlyDictionary<string, string> EmptyMetadata =
 425        new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(StringComparer.Ordinal));
 26
 3227    private GovernedOperationExecutionReceipt(
 3228        string operationExecutionId,
 3229        string? executionAttemptId,
 3230        GovernedOperationPersistenceOutcome persistenceOutcome,
 3231        string? mutationBatchId,
 3232        int mutationRecordCount,
 3233        string? mutationManifestHash,
 3234        string? mutationManifestAlgorithm,
 3235        DateTimeOffset completedUtc,
 3236        string? persistenceProvider,
 3237        string? decisionAuditRecordId,
 3238        IReadOnlyDictionary<string, string> metadata)
 39    {
 3240        OperationExecutionId = operationExecutionId;
 3241        ExecutionAttemptId = executionAttemptId;
 3242        PersistenceOutcome = persistenceOutcome;
 3243        MutationBatchId = mutationBatchId;
 3244        MutationRecordCount = mutationRecordCount;
 3245        MutationManifestHash = mutationManifestHash;
 3246        MutationManifestAlgorithm = mutationManifestAlgorithm;
 3247        CompletedUtc = completedUtc;
 3248        PersistenceProvider = persistenceProvider;
 3249        DecisionAuditRecordId = decisionAuditRecordId;
 3250        Metadata = metadata;
 3251    }
 52
 53    /// <summary>Gets the logical execution identifier shared across retries.</summary>
 3654    public string OperationExecutionId { get; }
 55
 56    /// <summary>Gets the identifier for this execution attempt, when supplied.</summary>
 4057    public string? ExecutionAttemptId { get; }
 58
 59    /// <summary>Gets the host persistence outcome.</summary>
 10660    public GovernedOperationPersistenceOutcome PersistenceOutcome { get; }
 61
 62    /// <summary>Gets the opaque host mutation batch identifier, when persistence committed.</summary>
 3263    public string? MutationBatchId { get; }
 64
 65    /// <summary>Gets the number of host-owned mutation records represented by the batch.</summary>
 3266    public int MutationRecordCount { get; }
 67
 68    /// <summary>Gets the canonical privacy-safe mutation manifest hash, when persistence committed.</summary>
 3069    public string? MutationManifestHash { get; }
 70
 71    /// <summary>Gets the mutation manifest hash algorithm, when persistence committed.</summary>
 2672    public string? MutationManifestAlgorithm { get; }
 73
 74    /// <summary>Gets the UTC timestamp when execution completed.</summary>
 2475    public DateTimeOffset CompletedUtc { get; }
 76
 77    /// <summary>Gets the host persistence provider or provider family, when supplied.</summary>
 2678    public string? PersistenceProvider { get; }
 79
 80    /// <summary>Gets the persisted AsiBackbone decision audit record identifier, when supplied.</summary>
 2681    public string? DecisionAuditRecordId { get; }
 82
 83    /// <summary>Gets minimized host metadata associated with the receipt.</summary>
 2684    public IReadOnlyDictionary<string, string> Metadata { get; }
 85
 86    /// <summary>Gets a value indicating whether execution completed without persisted mutation.</summary>
 3087    public bool CompletedWithoutMutation => PersistenceOutcome == GovernedOperationPersistenceOutcome.CompletedWithoutMu
 88
 89    /// <summary>Gets a value indicating whether the receipt contains a committed mutation binding.</summary>
 1690    public bool HasCommittedMutation => PersistenceOutcome == GovernedOperationPersistenceOutcome.Committed;
 91
 92    /// <summary>
 93    /// Creates a validated execution completion receipt.
 94    /// </summary>
 95    public static GovernedOperationExecutionReceipt Create(
 96        string operationExecutionId,
 97        GovernedOperationPersistenceOutcome persistenceOutcome,
 98        string? executionAttemptId = null,
 99        string? mutationBatchId = null,
 100        int mutationRecordCount = 0,
 101        string? mutationManifestHash = null,
 102        string? mutationManifestAlgorithm = null,
 103        DateTimeOffset? completedUtc = null,
 104        string? persistenceProvider = null,
 105        string? decisionAuditRecordId = null,
 106        IReadOnlyDictionary<string, string>? metadata = null)
 107    {
 36108        if (!Enum.IsDefined(persistenceOutcome))
 109        {
 0110            throw new ArgumentOutOfRangeException(nameof(persistenceOutcome), persistenceOutcome, "Persistence outcome m
 111        }
 112
 36113        if (mutationRecordCount < 0)
 114        {
 0115            throw new ArgumentOutOfRangeException(nameof(mutationRecordCount), mutationRecordCount, "Mutation record cou
 116        }
 117
 36118        string normalizedOperationExecutionId = NormalizeRequired(operationExecutionId, nameof(operationExecutionId), Ma
 36119        string? normalizedExecutionAttemptId = NormalizeOptional(executionAttemptId, nameof(executionAttemptId), Maximum
 36120        string? normalizedMutationBatchId = NormalizeOptional(mutationBatchId, nameof(mutationBatchId), MaximumIdentifie
 36121        string? normalizedManifestHash = NormalizeOptional(mutationManifestHash, nameof(mutationManifestHash), MaximumHa
 36122        string? normalizedManifestAlgorithm = NormalizeOptional(mutationManifestAlgorithm, nameof(mutationManifestAlgori
 36123        string? normalizedPersistenceProvider = NormalizeOptional(persistenceProvider, nameof(persistenceProvider), Maxi
 36124        string? normalizedDecisionAuditRecordId = NormalizeOptional(decisionAuditRecordId, nameof(decisionAuditRecordId)
 125
 36126        ValidateMutationBinding(
 36127            persistenceOutcome,
 36128            mutationRecordCount,
 36129            normalizedMutationBatchId,
 36130            normalizedManifestHash,
 36131            normalizedManifestAlgorithm);
 132
 32133        return new GovernedOperationExecutionReceipt(
 32134            normalizedOperationExecutionId,
 32135            normalizedExecutionAttemptId,
 32136            persistenceOutcome,
 32137            normalizedMutationBatchId,
 32138            mutationRecordCount,
 32139            normalizedManifestHash,
 32140            normalizedManifestAlgorithm,
 32141            (completedUtc ?? DateTimeOffset.UtcNow).ToUniversalTime(),
 32142            normalizedPersistenceProvider,
 32143            normalizedDecisionAuditRecordId,
 32144            NormalizeMetadata(metadata));
 145    }
 146
 147    /// <summary>
 148    /// Builds lifecycle metadata from the typed receipt and optional minimized host metadata.
 149    /// </summary>
 150    public IReadOnlyDictionary<string, string> ToLifecycleMetadata(IReadOnlyDictionary<string, string>? metadata = null)
 151    {
 22152        SortedDictionary<string, string> values = new(StringComparer.Ordinal);
 153
 22154        AddMetadata(values, Metadata);
 22155        AddMetadata(values, metadata);
 156
 22157        values[HostAccountabilityMetadataKeys.OperationExecutionId] = OperationExecutionId;
 22158        values[HostAccountabilityMetadataKeys.MutationRecordCount] = MutationRecordCount.ToString(CultureInfo.InvariantC
 22159        values[HostAccountabilityMetadataKeys.PersistenceOutcome] = PersistenceOutcome.ToString();
 22160        values[HostAccountabilityMetadataKeys.CompletedWithoutMutation] = CompletedWithoutMutation ? "true" : "false";
 161
 22162        AddOptional(values, HostAccountabilityMetadataKeys.ExecutionAttemptId, ExecutionAttemptId);
 22163        AddOptional(values, HostAccountabilityMetadataKeys.MutationBatchId, MutationBatchId);
 22164        AddOptional(values, HostAccountabilityMetadataKeys.MutationManifestHash, MutationManifestHash);
 22165        AddOptional(values, HostAccountabilityMetadataKeys.MutationManifestAlgorithm, MutationManifestAlgorithm);
 22166        AddOptional(values, HostAccountabilityMetadataKeys.PersistenceProvider, PersistenceProvider);
 22167        AddOptional(values, HostAccountabilityMetadataKeys.DecisionAuditRecordId, DecisionAuditRecordId);
 168
 22169        return new ReadOnlyDictionary<string, string>(values);
 170    }
 171
 172    private static void ValidateMutationBinding(
 173        GovernedOperationPersistenceOutcome outcome,
 174        int mutationRecordCount,
 175        string? mutationBatchId,
 176        string? mutationManifestHash,
 177        string? mutationManifestAlgorithm)
 178    {
 36179        bool hasAnyBinding = mutationBatchId is not null || mutationManifestHash is not null || mutationManifestAlgorith
 180
 36181        if (outcome == GovernedOperationPersistenceOutcome.Committed)
 182        {
 20183            if (mutationRecordCount < 1)
 184            {
 0185                throw new ArgumentException("A committed persistence outcome requires at least one mutation record.", na
 186            }
 187
 20188            if (mutationBatchId is null || mutationManifestHash is null || mutationManifestAlgorithm is null)
 189            {
 2190                throw new ArgumentException("A committed persistence outcome requires a mutation batch identifier, manif
 191            }
 192
 18193            return;
 194        }
 195
 16196        if (mutationRecordCount != 0 || hasAnyBinding)
 197        {
 2198            throw new ArgumentException("Failed, rolled-back, and no-mutation outcomes cannot claim a committed mutation
 199        }
 14200    }
 201
 202    private static string NormalizeRequired(string value, string parameterName, int maximumLength)
 203    {
 80204        ArgumentException.ThrowIfNullOrWhiteSpace(value, parameterName);
 80205        return Normalize(value, parameterName, maximumLength);
 206    }
 207
 208    private static string? NormalizeOptional(string? value, string parameterName, int maximumLength)
 209    {
 216210        return string.IsNullOrWhiteSpace(value) ? null : Normalize(value, parameterName, maximumLength);
 211    }
 212
 213    private static string Normalize(string value, string parameterName, int maximumLength)
 214    {
 274215        string normalized = value.Trim();
 274216        return normalized.Length > maximumLength
 274217            ? throw new ArgumentOutOfRangeException(parameterName, value, $"Value cannot exceed {maximumLength} characte
 274218            : normalized.Any(char.IsControl)
 274219            ? throw new ArgumentException("Value cannot contain control characters.", parameterName)
 274220            : normalized;
 221    }
 222
 223    private static IReadOnlyDictionary<string, string> NormalizeMetadata(IReadOnlyDictionary<string, string>? metadata)
 224    {
 76225        if (metadata is null || metadata.Count == 0)
 226        {
 54227            return EmptyMetadata;
 228        }
 229
 22230        SortedDictionary<string, string> normalized = new(StringComparer.Ordinal);
 132231        foreach (KeyValuePair<string, string> item in metadata)
 232        {
 44233            string key = NormalizeRequired(item.Key, nameof(metadata), MaximumMetadataKeyLength);
 44234            string value = Normalize(item.Value ?? string.Empty, nameof(metadata), MaximumMetadataValueLength);
 44235            normalized[key] = value;
 236        }
 237
 22238        return new ReadOnlyDictionary<string, string>(normalized);
 239    }
 240
 241    private static void AddMetadata(SortedDictionary<string, string> destination, IReadOnlyDictionary<string, string>? m
 242    {
 44243        IReadOnlyDictionary<string, string> normalized = NormalizeMetadata(metadata);
 160244        foreach (KeyValuePair<string, string> item in normalized)
 245        {
 36246            destination[item.Key] = item.Value;
 247        }
 44248    }
 249
 250    private static void AddOptional(SortedDictionary<string, string> destination, string key, string? value)
 251    {
 132252        if (value is not null)
 253        {
 106254            destination[key] = value;
 255        }
 132256    }
 257}

Methods/Properties

.cctor()
.ctor(System.String,System.String,AsiBackbone.Core.HostIntegration.GovernedOperationPersistenceOutcome,System.String,System.Int32,System.String,System.String,System.DateTimeOffset,System.String,System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
get_OperationExecutionId()
get_ExecutionAttemptId()
get_PersistenceOutcome()
get_MutationBatchId()
get_MutationRecordCount()
get_MutationManifestHash()
get_MutationManifestAlgorithm()
get_CompletedUtc()
get_PersistenceProvider()
get_DecisionAuditRecordId()
get_Metadata()
get_CompletedWithoutMutation()
get_HasCommittedMutation()
Create(System.String,AsiBackbone.Core.HostIntegration.GovernedOperationPersistenceOutcome,System.String,System.String,System.Int32,System.String,System.String,System.Nullable`1<System.DateTimeOffset>,System.String,System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
ToLifecycleMetadata(System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
ValidateMutationBinding(AsiBackbone.Core.HostIntegration.GovernedOperationPersistenceOutcome,System.Int32,System.String,System.String,System.String)
NormalizeRequired(System.String,System.String,System.Int32)
NormalizeOptional(System.String,System.String,System.Int32)
Normalize(System.String,System.String,System.Int32)
NormalizeMetadata(System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
AddMetadata(System.Collections.Generic.SortedDictionary`2<System.String,System.String>,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
AddOptional(System.Collections.Generic.SortedDictionary`2<System.String,System.String>,System.String,System.String)