< Summary

Information
Class: AsiBackbone.Samples.NcatAuditCompletionAdapter.NcatAuditCompletionAdapter
Assembly: AsiBackbone.Samples.NcatAuditCompletionAdapter
File(s): /home/runner/work/AsiBackbone/AsiBackbone/samples/NcatAuditCompletionAdapter/NcatAuditCompletionAdapter.cs
Line coverage
96%
Covered lines: 152
Uncovered lines: 5
Coverable lines: 157
Total lines: 290
Line coverage: 96.8%
Branch coverage
76%
Covered branches: 60
Total branches: 78
Branch coverage: 76.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)66.67%66100%
DeliverAsync()83.33%121295.65%
ValidateHandoff(...)75%1212100%
ValidateDecisionCorrelation(...)75%88100%
PersistenceFailure(...)100%66100%
TryMapOutcome(...)94.44%181891.67%
CreateLifecycleEventId(...)100%11100%
Equivalent(...)50%121295.65%
GetMetadata(...)50%22100%
NormalizeOptional(...)50%22100%
Terminal(...)100%11100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/samples/NcatAuditCompletionAdapter/NcatAuditCompletionAdapter.cs

#LineLine coverage
 1using System.Security.Cryptography;
 2using System.Text;
 3using AsiBackbone.Core.Audit;
 4using AsiBackbone.Core.HostIntegration;
 5
 6namespace AsiBackbone.Samples.NcatAuditCompletionAdapter;
 7
 8/// <summary>
 9/// Maps a minimized NCAT audit-completion handoff into AsiBackbone governed execution evidence.
 10/// </summary>
 11/// <remarks>
 12/// This reference adapter intentionally depends only on AsiBackbone Core and the source-neutral
 13/// <see cref="NcatAuditCompletionHandoff" /> contract. A consuming host may translate its NCAT
 14/// completion receipt or completion-outbox entry into that contract without coupling either core product.
 15/// </remarks>
 16public sealed class NcatAuditCompletionAdapter
 17{
 18    private const string SourceCompletionEntryIdMetadataKey = "ncatCompletionEntryId";
 19    private const string SourceAdapterMetadataKey = "completionAdapter";
 20    private const string SourceAdapterMetadataValue = "NCAT";
 21    private const string LifecycleEventPrefix = "ncat-completion-";
 22
 23    private readonly IDecisionReceiptLifecycleStore lifecycleStore;
 24    private readonly INcatDecisionReceiptResolver decisionReceiptResolver;
 25    private readonly NcatAuditCompletionAdapterOptions options;
 26
 27    /// <summary>
 28    /// Initializes a new optional NCAT audit-completion adapter.
 29    /// </summary>
 2130    public NcatAuditCompletionAdapter(
 2131        IDecisionReceiptLifecycleStore lifecycleStore,
 2132        INcatDecisionReceiptResolver decisionReceiptResolver,
 2133        NcatAuditCompletionAdapterOptions? options = null)
 34    {
 2135        this.lifecycleStore = lifecycleStore ?? throw new ArgumentNullException(nameof(lifecycleStore));
 2136        this.decisionReceiptResolver = decisionReceiptResolver ?? throw new ArgumentNullException(nameof(decisionReceipt
 2137        this.options = options ?? new NcatAuditCompletionAdapterOptions();
 2138        this.options.Validate();
 2139    }
 40
 41    /// <summary>
 42    /// Attempts to append the governed completion lifecycle event before the source entry is acknowledged.
 43    /// </summary>
 44    public async ValueTask<NcatAuditCompletionDeliveryResult> DeliverAsync(
 45        NcatAuditCompletionHandoff handoff,
 46        CancellationToken cancellationToken = default)
 47    {
 2648        ArgumentNullException.ThrowIfNull(handoff);
 2649        cancellationToken.ThrowIfCancellationRequested();
 50
 2651        NcatAuditCompletionDeliveryResult? validationFailure = ValidateHandoff(handoff);
 2652        if (validationFailure is not null)
 53        {
 654            return validationFailure;
 55        }
 56
 2057        if (!TryMapOutcome(handoff.PersistenceOutcome, out GovernedOperationPersistenceOutcome persistenceOutcome))
 58        {
 059            return Terminal("unsupported-persistence-outcome");
 60        }
 61
 2062        IDecisionReceipt? decisionReceipt = await decisionReceiptResolver.ResolveAsync(
 2063            handoff.DecisionAuditRecordId!.Trim(),
 2064            NormalizeOptional(handoff.CorrelationId),
 2065            cancellationToken).ConfigureAwait(false);
 66
 2067        if (decisionReceipt is null)
 68        {
 169            return new NcatAuditCompletionDeliveryResult(
 170                NcatAuditCompletionDeliveryDisposition.Deferred,
 171                "decision-residue-not-available");
 72        }
 73
 1974        NcatAuditCompletionDeliveryResult? correlationFailure = ValidateDecisionCorrelation(handoff, decisionReceipt);
 1975        if (correlationFailure is not null)
 76        {
 177            return correlationFailure;
 78        }
 79
 80        GovernedOperationExecutionReceipt executionReceipt;
 81        try
 82        {
 1883            executionReceipt = GovernedOperationExecutionReceipt.Create(
 1884                operationExecutionId: handoff.OperationExecutionId!,
 1885                persistenceOutcome: persistenceOutcome,
 1886                executionAttemptId: handoff.ExecutionAttemptId,
 1887                mutationBatchId: handoff.MutationBatchId,
 1888                mutationRecordCount: handoff.AuditRecordCount,
 1889                mutationManifestHash: handoff.MutationManifestHash,
 1890                mutationManifestAlgorithm: handoff.MutationManifestAlgorithm,
 1891                completedUtc: handoff.CompletedUtc,
 1892                persistenceProvider: options.PersistenceProvider,
 1893                decisionAuditRecordId: handoff.DecisionAuditRecordId);
 1494        }
 495        catch (ArgumentException exception)
 96        {
 497            return Terminal("invalid-execution-receipt", exception.GetType().Name);
 98        }
 99
 14100        string lifecycleEventId = CreateLifecycleEventId(handoff.CompletionEntryId);
 14101        IReadOnlyDictionary<string, string> metadata = new Dictionary<string, string>(StringComparer.Ordinal)
 14102        {
 14103            [SourceCompletionEntryIdMetadataKey] = handoff.CompletionEntryId.Trim(),
 14104            [SourceAdapterMetadataKey] = SourceAdapterMetadataValue
 14105        };
 106
 14107        DecisionReceiptLifecycleEvent lifecycleEvent = HostAccountabilityLifecycleEvent.FromExecutionReceipt(
 14108            decisionReceipt,
 14109            executionReceipt,
 14110            eventId: lifecycleEventId,
 14111            metadata: metadata);
 112
 14113        DecisionReceiptLifecycleEvent? existing = await lifecycleStore.FindByEventIdAsync(
 14114            lifecycleEventId,
 14115            cancellationToken).ConfigureAwait(false);
 116
 14117        if (existing is not null)
 118        {
 2119            return Equivalent(existing, lifecycleEvent)
 2120                ? new NcatAuditCompletionDeliveryResult(
 2121                    NcatAuditCompletionDeliveryDisposition.Duplicate,
 2122                    "completion-already-delivered",
 2123                    lifecycleEventId,
 2124                    executionReceipt,
 2125                    existing)
 2126                : Terminal("idempotency-conflict", lifecycleEventId: lifecycleEventId);
 127        }
 128
 129        try
 130        {
 12131            DecisionReceiptLifecycleEvent appended = await lifecycleStore.AppendAsync(
 12132                lifecycleEvent,
 12133                cancellationToken).ConfigureAwait(false);
 134
 10135            return new NcatAuditCompletionDeliveryResult(
 10136                NcatAuditCompletionDeliveryDisposition.Delivered,
 10137                "lifecycle-event-appended",
 10138                appended.EventId,
 10139                executionReceipt,
 10140                appended);
 141        }
 0142        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 143        {
 0144            throw;
 145        }
 2146        catch (Exception exception)
 147        {
 2148            return PersistenceFailure(handoff, lifecycleEventId, executionReceipt, lifecycleEvent, exception);
 149        }
 26150    }
 151
 152    private static NcatAuditCompletionDeliveryResult? ValidateHandoff(NcatAuditCompletionHandoff handoff)
 153    {
 26154        return string.IsNullOrWhiteSpace(handoff.CompletionEntryId)
 26155            ? Terminal("completion-entry-id-required")
 26156            : string.IsNullOrWhiteSpace(handoff.OperationExecutionId)
 26157            ? Terminal("operation-execution-id-required")
 26158            : string.IsNullOrWhiteSpace(handoff.DecisionAuditRecordId)
 26159            ? Terminal("decision-audit-record-id-required")
 26160            : string.IsNullOrWhiteSpace(handoff.PersistenceOutcome)
 26161            ? Terminal("persistence-outcome-required")
 26162            : handoff.AuditRecordCount < 0
 26163            ? Terminal("audit-record-count-invalid")
 26164            : handoff.DeliveryAttempt < 0
 26165            ? Terminal("delivery-attempt-invalid")
 26166            : null;
 167    }
 168
 169    private static NcatAuditCompletionDeliveryResult? ValidateDecisionCorrelation(
 170        NcatAuditCompletionHandoff handoff,
 171        IDecisionReceipt decisionReceipt)
 172    {
 19173        string? correlationId = NormalizeOptional(handoff.CorrelationId);
 19174        if (correlationId is not null &&
 19175            !string.Equals(correlationId, decisionReceipt.CorrelationId, StringComparison.Ordinal))
 176        {
 1177            return Terminal("correlation-id-mismatch");
 178        }
 179
 18180        string? traceId = NormalizeOptional(handoff.TraceId);
 18181        return traceId is not null &&
 18182            !string.Equals(traceId, decisionReceipt.TraceId, StringComparison.Ordinal)
 18183            ? Terminal("trace-id-mismatch")
 18184            : null;
 185    }
 186
 187    private NcatAuditCompletionDeliveryResult PersistenceFailure(
 188        NcatAuditCompletionHandoff handoff,
 189        string lifecycleEventId,
 190        GovernedOperationExecutionReceipt executionReceipt,
 191        DecisionReceiptLifecycleEvent lifecycleEvent,
 192        Exception exception)
 193    {
 2194        bool deadLettered = options.DeadLetterAfterAttempts is int threshold &&
 2195            handoff.DeliveryAttempt >= threshold;
 196
 2197        return new NcatAuditCompletionDeliveryResult(
 2198            deadLettered
 2199                ? NcatAuditCompletionDeliveryDisposition.DeadLetter
 2200                : NcatAuditCompletionDeliveryDisposition.Retryable,
 2201            deadLettered
 2202                ? "lifecycle-persistence-dead-lettered"
 2203                : "lifecycle-persistence-failed",
 2204            lifecycleEventId,
 2205            executionReceipt,
 2206            lifecycleEvent,
 2207            exception.GetType().Name);
 208    }
 209
 210    private static bool TryMapOutcome(
 211        string sourceOutcome,
 212        out GovernedOperationPersistenceOutcome persistenceOutcome)
 213    {
 20214        string normalized = string.Concat(sourceOutcome
 20215            .Where(char.IsLetterOrDigit)
 20216            .Select(char.ToLowerInvariant));
 217
 20218        persistenceOutcome = normalized switch
 20219        {
 10220            "committed" => GovernedOperationPersistenceOutcome.Committed,
 3221            "failed" => GovernedOperationPersistenceOutcome.Failed,
 3222            "rolledback" => GovernedOperationPersistenceOutcome.RolledBack,
 4223            "completedwithoutmutation" or "nomutation" => GovernedOperationPersistenceOutcome.CompletedWithoutMutation,
 0224            _ => default
 20225        };
 226
 20227        return normalized is "committed" or "failed" or "rolledback" or "completedwithoutmutation" or "nomutation";
 228    }
 229
 230    private static string CreateLifecycleEventId(string completionEntryId)
 231    {
 14232        byte[] digest = SHA256.HashData(Encoding.UTF8.GetBytes(completionEntryId.Trim()));
 14233        return string.Concat(LifecycleEventPrefix, Convert.ToHexString(digest).ToLowerInvariant());
 234    }
 235
 236    private static bool Equivalent(
 237        DecisionReceiptLifecycleEvent existing,
 238        DecisionReceiptLifecycleEvent candidate)
 239    {
 2240        if (existing.Stage != candidate.Stage ||
 2241            !string.Equals(existing.CorrelationId, candidate.CorrelationId, StringComparison.Ordinal) ||
 2242            !string.Equals(existing.DecisionReceiptId, candidate.DecisionReceiptId, StringComparison.Ordinal) ||
 2243            !string.Equals(existing.TraceId, candidate.TraceId, StringComparison.Ordinal) ||
 2244            !string.Equals(existing.OperationName, candidate.OperationName, StringComparison.Ordinal) ||
 2245            !string.Equals(existing.Outcome, candidate.Outcome, StringComparison.Ordinal))
 246        {
 0247            return false;
 248        }
 249
 2250        string[] keys =
 2251        [
 2252            SourceCompletionEntryIdMetadataKey,
 2253            HostAccountabilityMetadataKeys.OperationExecutionId,
 2254            HostAccountabilityMetadataKeys.ExecutionAttemptId,
 2255            HostAccountabilityMetadataKeys.DecisionAuditRecordId,
 2256            HostAccountabilityMetadataKeys.PersistenceOutcome,
 2257            HostAccountabilityMetadataKeys.MutationBatchId,
 2258            HostAccountabilityMetadataKeys.MutationRecordCount,
 2259            HostAccountabilityMetadataKeys.MutationManifestHash,
 2260            HostAccountabilityMetadataKeys.MutationManifestAlgorithm
 2261        ];
 262
 2263        return keys.All(key => string.Equals(
 2264            GetMetadata(existing.Metadata, key),
 2265            GetMetadata(candidate.Metadata, key),
 2266            StringComparison.Ordinal));
 267    }
 268
 269    private static string? GetMetadata(IReadOnlyDictionary<string, string> metadata, string key)
 270    {
 36271        return metadata.TryGetValue(key, out string? value) ? value : null;
 272    }
 273
 274    private static string? NormalizeOptional(string? value)
 275    {
 57276        return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
 277    }
 278
 279    private static NcatAuditCompletionDeliveryResult Terminal(
 280        string reasonCode,
 281        string? failureType = null,
 282        string? lifecycleEventId = null)
 283    {
 11284        return new NcatAuditCompletionDeliveryResult(
 11285            NcatAuditCompletionDeliveryDisposition.Terminal,
 11286            reasonCode,
 11287            lifecycleEventId,
 11288            FailureType: failureType);
 289    }
 290}