< 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: 107
Uncovered lines: 3
Coverable lines: 110
Total lines: 266
Line coverage: 97.2%
Branch coverage
84%
Covered branches: 39
Total branches: 46
Branch coverage: 84.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
get_CompletedWithoutMutation()100%11100%
get_HasCommittedMutation()100%11100%
Create(...)75%8893.1%
ToLifecycleMetadata(...)100%22100%
ValidateMutationBinding(...)87.5%161690%
NormalizeRequired(...)100%11100%
NormalizeOptional(...)100%22100%
Normalize(...)50%44100%
NormalizeMetadata(...)90%1010100%
AddMetadata(...)100%22100%
AddOptional(...)100%22100%

File(s)

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

#LineLine coverage
 1using System.Collections.ObjectModel;
 2using System.Globalization;
 3using AsiBackbone.Core.Signing;
 4
 5namespace AsiBackbone.Core.HostIntegration;
 6
 7/// <summary>
 8/// Represents a framework-neutral completion receipt for one governed host execution attempt.
 9/// </summary>
 10/// <remarks>
 11/// The receipt binds governance lifecycle evidence to host-owned persistence without copying raw
 12/// original or current application values into AsiBackbone. Mutation details remain authoritative
 13/// in the host audit store; this receipt carries only opaque identifiers, counts, hashes, and
 14/// minimized metadata.
 15/// </remarks>
 16public sealed class GovernedOperationExecutionReceipt
 17{
 18    private const int MaximumIdentifierLength = 256;
 19    private const int MaximumProviderLength = 256;
 20    private const int MaximumHashLength = 1024;
 21    private const int MaximumAlgorithmLength = 128;
 22    private const int MaximumMetadataKeyLength = 128;
 23    private const int MaximumMetadataValueLength = 2048;
 24
 225    private static readonly IReadOnlyDictionary<string, string> EmptyMetadata =
 226        new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(StringComparer.Ordinal));
 27
 2628    private GovernedOperationExecutionReceipt(
 2629        string operationExecutionId,
 2630        string? executionAttemptId,
 2631        GovernedOperationPersistenceOutcome persistenceOutcome,
 2632        string? mutationBatchId,
 2633        int mutationRecordCount,
 2634        string? mutationManifestHash,
 2635        string? mutationManifestAlgorithm,
 2636        DateTimeOffset completedUtc,
 2637        string? persistenceProvider,
 2638        string? decisionAuditRecordId,
 2639        IReadOnlyDictionary<string, string> metadata)
 40    {
 2641        OperationExecutionId = operationExecutionId;
 2642        ExecutionAttemptId = executionAttemptId;
 2643        PersistenceOutcome = persistenceOutcome;
 2644        MutationBatchId = mutationBatchId;
 2645        MutationRecordCount = mutationRecordCount;
 2646        MutationManifestHash = mutationManifestHash;
 2647        MutationManifestAlgorithm = mutationManifestAlgorithm;
 2648        CompletedUtc = completedUtc;
 2649        PersistenceProvider = persistenceProvider;
 2650        DecisionAuditRecordId = decisionAuditRecordId;
 2651        Metadata = metadata;
 2652    }
 53
 54    /// <summary>Gets the logical execution identifier shared across retries.</summary>
 55    public string OperationExecutionId { get; }
 56
 57    /// <summary>Gets the identifier for this execution attempt, when supplied.</summary>
 58    public string? ExecutionAttemptId { get; }
 59
 60    /// <summary>Gets the host persistence outcome.</summary>
 61    public GovernedOperationPersistenceOutcome PersistenceOutcome { get; }
 62
 63    /// <summary>Gets the opaque host mutation batch identifier, when persistence committed.</summary>
 64    public string? MutationBatchId { get; }
 65
 66    /// <summary>Gets the number of host-owned mutation records represented by the batch.</summary>
 67    public int MutationRecordCount { get; }
 68
 69    /// <summary>Gets the canonical privacy-safe mutation manifest hash, when persistence committed.</summary>
 70    public string? MutationManifestHash { get; }
 71
 72    /// <summary>Gets the mutation manifest hash algorithm, when persistence committed.</summary>
 73    public string? MutationManifestAlgorithm { get; }
 74
 75    /// <summary>Gets the UTC timestamp when execution completed.</summary>
 76    public DateTimeOffset CompletedUtc { get; }
 77
 78    /// <summary>Gets the host persistence provider or provider family, when supplied.</summary>
 79    public string? PersistenceProvider { get; }
 80
 81    /// <summary>Gets the persisted AsiBackbone decision audit record identifier, when supplied.</summary>
 82    public string? DecisionAuditRecordId { get; }
 83
 84    /// <summary>Gets minimized host metadata associated with the receipt.</summary>
 85    public IReadOnlyDictionary<string, string> Metadata { get; }
 86
 87    /// <summary>Gets a value indicating whether execution completed without persisted mutation.</summary>
 3388    public bool CompletedWithoutMutation => PersistenceOutcome == GovernedOperationPersistenceOutcome.CompletedWithoutMu
 89
 90    /// <summary>Gets a value indicating whether the receipt contains a committed mutation binding.</summary>
 1291    public bool HasCommittedMutation => PersistenceOutcome == GovernedOperationPersistenceOutcome.Committed;
 92
 93    /// <summary>
 94    /// Creates a validated execution completion receipt.
 95    /// </summary>
 96    public static GovernedOperationExecutionReceipt Create(
 97        string operationExecutionId,
 98        GovernedOperationPersistenceOutcome persistenceOutcome,
 99        string? executionAttemptId = null,
 100        string? mutationBatchId = null,
 101        int mutationRecordCount = 0,
 102        string? mutationManifestHash = null,
 103        string? mutationManifestAlgorithm = null,
 104        DateTimeOffset? completedUtc = null,
 105        string? persistenceProvider = null,
 106        string? decisionAuditRecordId = null,
 107        IReadOnlyDictionary<string, string>? metadata = null)
 108    {
 32109        if (!Enum.IsDefined(persistenceOutcome))
 110        {
 0111            throw new ArgumentOutOfRangeException(nameof(persistenceOutcome), persistenceOutcome, "Persistence outcome m
 112        }
 113
 32114        if (mutationRecordCount < 0)
 115        {
 0116            throw new ArgumentOutOfRangeException(nameof(mutationRecordCount), mutationRecordCount, "Mutation record cou
 117        }
 118
 32119        string normalizedOperationExecutionId = NormalizeRequired(operationExecutionId, nameof(operationExecutionId), Ma
 32120        string? normalizedExecutionAttemptId = NormalizeOptional(executionAttemptId, nameof(executionAttemptId), Maximum
 32121        string? normalizedMutationBatchId = NormalizeOptional(mutationBatchId, nameof(mutationBatchId), MaximumIdentifie
 32122        string? normalizedManifestHash = NormalizeOptional(mutationManifestHash, nameof(mutationManifestHash), MaximumHa
 32123        string? normalizedManifestAlgorithm = NormalizeOptional(mutationManifestAlgorithm, nameof(mutationManifestAlgori
 32124        string? normalizedPersistenceProvider = NormalizeOptional(persistenceProvider, nameof(persistenceProvider), Maxi
 32125        string? normalizedDecisionAuditRecordId = NormalizeOptional(decisionAuditRecordId, nameof(decisionAuditRecordId)
 126
 32127        ValidateMutationBinding(
 32128            persistenceOutcome,
 32129            mutationRecordCount,
 32130            normalizedMutationBatchId,
 32131            normalizedManifestHash,
 32132            normalizedManifestAlgorithm);
 133
 27134        return new GovernedOperationExecutionReceipt(
 27135            normalizedOperationExecutionId,
 27136            normalizedExecutionAttemptId,
 27137            persistenceOutcome,
 27138            normalizedMutationBatchId,
 27139            mutationRecordCount,
 27140            normalizedManifestHash,
 27141            normalizedManifestAlgorithm,
 27142            (completedUtc ?? DateTimeOffset.UtcNow).ToUniversalTime(),
 27143            normalizedPersistenceProvider,
 27144            normalizedDecisionAuditRecordId,
 27145            NormalizeMetadata(metadata));
 146    }
 147
 148    /// <summary>
 149    /// Builds lifecycle metadata from the typed receipt and optional minimized host metadata.
 150    /// </summary>
 151    public IReadOnlyDictionary<string, string> ToLifecycleMetadata(IReadOnlyDictionary<string, string>? metadata = null)
 152    {
 24153        SortedDictionary<string, string> values = new(StringComparer.Ordinal);
 154
 24155        AddMetadata(values, Metadata);
 24156        AddMetadata(values, metadata);
 157
 24158        values[HostAccountabilityMetadataKeys.OperationExecutionId] = OperationExecutionId;
 24159        values[HostAccountabilityMetadataKeys.MutationRecordCount] = MutationRecordCount.ToString(CultureInfo.InvariantC
 24160        values[HostAccountabilityMetadataKeys.PersistenceOutcome] =
 24161            CanonicalEnumWireNames.ForPersistenceOutcome(PersistenceOutcome);
 24162        values[HostAccountabilityMetadataKeys.CompletedWithoutMutation] = CompletedWithoutMutation ? "true" : "false";
 163
 24164        AddOptional(values, HostAccountabilityMetadataKeys.ExecutionAttemptId, ExecutionAttemptId);
 24165        AddOptional(values, HostAccountabilityMetadataKeys.MutationBatchId, MutationBatchId);
 24166        AddOptional(values, HostAccountabilityMetadataKeys.MutationManifestHash, MutationManifestHash);
 24167        AddOptional(values, HostAccountabilityMetadataKeys.MutationManifestAlgorithm, MutationManifestAlgorithm);
 24168        AddOptional(values, HostAccountabilityMetadataKeys.PersistenceProvider, PersistenceProvider);
 24169        AddOptional(values, HostAccountabilityMetadataKeys.DecisionAuditRecordId, DecisionAuditRecordId);
 170
 24171        return new ReadOnlyDictionary<string, string>(values);
 172    }
 173
 174    private static void ValidateMutationBinding(
 175        GovernedOperationPersistenceOutcome outcome,
 176        int mutationRecordCount,
 177        string? mutationBatchId,
 178        string? mutationManifestHash,
 179        string? mutationManifestAlgorithm)
 180    {
 32181        bool hasAnyBinding = mutationBatchId is not null || mutationManifestHash is not null || mutationManifestAlgorith
 182
 32183        if (outcome == GovernedOperationPersistenceOutcome.Committed)
 184        {
 14185            if (mutationRecordCount < 1)
 186            {
 0187                throw new ArgumentException("A committed persistence outcome requires at least one mutation record.", na
 188            }
 189
 14190            if (mutationBatchId is null || mutationManifestHash is null || mutationManifestAlgorithm is null)
 191            {
 1192                throw new ArgumentException("A committed persistence outcome requires a mutation batch identifier, manif
 193            }
 194
 13195            return;
 196        }
 197
 18198        if (mutationRecordCount != 0 || hasAnyBinding)
 199        {
 4200            throw new ArgumentException("Failed, rolled-back, and no-mutation outcomes cannot claim a committed mutation
 201        }
 14202    }
 203
 204    private static string NormalizeRequired(string value, string parameterName, int maximumLength)
 205    {
 68206        ArgumentException.ThrowIfNullOrWhiteSpace(value, parameterName);
 68207        return Normalize(value, parameterName, maximumLength);
 208    }
 209
 210    private static string? NormalizeOptional(string? value, string parameterName, int maximumLength)
 211    {
 192212        return string.IsNullOrWhiteSpace(value) ? null : Normalize(value, parameterName, maximumLength);
 213    }
 214
 215    private static string Normalize(string value, string parameterName, int maximumLength)
 216    {
 227217        string normalized = value.Trim();
 227218        return normalized.Length > maximumLength
 227219            ? throw new ArgumentOutOfRangeException(parameterName, value, $"Value cannot exceed {maximumLength} characte
 227220            : normalized.Any(char.IsControl)
 227221            ? throw new ArgumentException("Value cannot contain control characters.", parameterName)
 227222            : normalized;
 223    }
 224
 225    private static IReadOnlyDictionary<string, string> NormalizeMetadata(IReadOnlyDictionary<string, string>? metadata)
 226    {
 75227        if (metadata is null || metadata.Count == 0)
 228        {
 57229            return EmptyMetadata;
 230        }
 231
 18232        SortedDictionary<string, string> normalized = new(StringComparer.Ordinal);
 107233        foreach (KeyValuePair<string, string> item in metadata)
 234        {
 36235            string key = NormalizeRequired(item.Key, nameof(metadata), MaximumMetadataKeyLength);
 36236            string value = Normalize(item.Value ?? string.Empty, nameof(metadata), MaximumMetadataValueLength);
 36237            if (normalized.ContainsKey(key))
 238            {
 1239                throw new ArgumentException(
 1240                    $"Execution receipt metadata contains keys that collide after trimming: '{key}'.",
 1241                    nameof(metadata));
 242            }
 243
 35244            normalized[key] = value;
 245        }
 246
 17247        return new ReadOnlyDictionary<string, string>(normalized);
 248    }
 249
 250    private static void AddMetadata(SortedDictionary<string, string> destination, IReadOnlyDictionary<string, string>? m
 251    {
 48252        IReadOnlyDictionary<string, string> normalized = NormalizeMetadata(metadata);
 152253        foreach (KeyValuePair<string, string> item in normalized)
 254        {
 28255            destination[item.Key] = item.Value;
 256        }
 48257    }
 258
 259    private static void AddOptional(SortedDictionary<string, string> destination, string key, string? value)
 260    {
 144261        if (value is not null)
 262        {
 80263            destination[key] = value;
 264        }
 144265    }
 266}