< 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: 61
Total branches: 80
Branch coverage: 76.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)66.66%66100%
DeliverAsync()83.33%121295.65%
ValidateHandoff(...)75%1212100%
ValidateDecisionCorrelation(...)75%88100%
PersistenceFailure(...)100%66100%
TryMapOutcome(...)90%202091.66%
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 IAsiBackboneAuditResidueLifecycleStore lifecycleStore;
 24    private readonly INcatDecisionResidueResolver decisionResidueResolver;
 25    private readonly NcatAuditCompletionAdapterOptions options;
 26
 27    /// <summary>
 28    /// Initializes a new optional NCAT audit-completion adapter.
 29    /// </summary>
 2830    public NcatAuditCompletionAdapter(
 2831        IAsiBackboneAuditResidueLifecycleStore lifecycleStore,
 2832        INcatDecisionResidueResolver decisionResidueResolver,
 2833        NcatAuditCompletionAdapterOptions? options = null)
 34    {
 2835        this.lifecycleStore = lifecycleStore ?? throw new ArgumentNullException(nameof(lifecycleStore));
 2836        this.decisionResidueResolver = decisionResidueResolver ?? throw new ArgumentNullException(nameof(decisionResidue
 2837        this.options = options ?? new NcatAuditCompletionAdapterOptions();
 2838        this.options.Validate();
 2839    }
 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    {
 3048        ArgumentNullException.ThrowIfNull(handoff);
 3049        cancellationToken.ThrowIfCancellationRequested();
 50
 3051        NcatAuditCompletionDeliveryResult? validationFailure = ValidateHandoff(handoff);
 3052        if (validationFailure is not null)
 53        {
 654            return validationFailure;
 55        }
 56
 2457        if (!TryMapOutcome(handoff.PersistenceOutcome, out GovernedOperationPersistenceOutcome persistenceOutcome))
 58        {
 059            return Terminal("unsupported-persistence-outcome");
 60        }
 61
 2462        IAsiBackboneAuditResidue? residue = await decisionResidueResolver.ResolveAsync(
 2463            handoff.DecisionAuditRecordId!.Trim(),
 2464            NormalizeOptional(handoff.CorrelationId),
 2465            cancellationToken).ConfigureAwait(false);
 66
 2467        if (residue is null)
 68        {
 269            return new NcatAuditCompletionDeliveryResult(
 270                NcatAuditCompletionDeliveryDisposition.Deferred,
 271                "decision-residue-not-available");
 72        }
 73
 2274        NcatAuditCompletionDeliveryResult? correlationFailure = ValidateDecisionCorrelation(handoff, residue);
 2275        if (correlationFailure is not null)
 76        {
 277            return correlationFailure;
 78        }
 79
 80        GovernedOperationExecutionReceipt receipt;
 81        try
 82        {
 2083            receipt = GovernedOperationExecutionReceipt.Create(
 2084                operationExecutionId: handoff.OperationExecutionId!,
 2085                persistenceOutcome: persistenceOutcome,
 2086                executionAttemptId: handoff.ExecutionAttemptId,
 2087                mutationBatchId: handoff.MutationBatchId,
 2088                mutationRecordCount: handoff.AuditRecordCount,
 2089                mutationManifestHash: handoff.MutationManifestHash,
 2090                mutationManifestAlgorithm: handoff.MutationManifestAlgorithm,
 2091                completedUtc: handoff.CompletedUtc,
 2092                persistenceProvider: options.PersistenceProvider,
 2093                decisionAuditRecordId: handoff.DecisionAuditRecordId);
 1894        }
 295        catch (ArgumentException exception)
 96        {
 297            return Terminal("invalid-execution-receipt", exception.GetType().Name);
 98        }
 99
 18100        string lifecycleEventId = CreateLifecycleEventId(handoff.CompletionEntryId);
 18101        IReadOnlyDictionary<string, string> metadata = new Dictionary<string, string>(StringComparer.Ordinal)
 18102        {
 18103            [SourceCompletionEntryIdMetadataKey] = handoff.CompletionEntryId.Trim(),
 18104            [SourceAdapterMetadataKey] = SourceAdapterMetadataValue
 18105        };
 106
 18107        AuditResidueLifecycleEvent lifecycleEvent = HostAccountabilityLifecycleEvent.FromExecutionReceipt(
 18108            residue,
 18109            receipt,
 18110            eventId: lifecycleEventId,
 18111            metadata: metadata);
 112
 18113        AuditResidueLifecycleEvent? existing = await lifecycleStore.FindByEventIdAsync(
 18114            lifecycleEventId,
 18115            cancellationToken).ConfigureAwait(false);
 116
 18117        if (existing is not null)
 118        {
 2119            return Equivalent(existing, lifecycleEvent)
 2120                ? new NcatAuditCompletionDeliveryResult(
 2121                    NcatAuditCompletionDeliveryDisposition.Duplicate,
 2122                    "completion-already-delivered",
 2123                    lifecycleEventId,
 2124                    receipt,
 2125                    existing)
 2126                : Terminal("idempotency-conflict", lifecycleEventId: lifecycleEventId);
 127        }
 128
 129        try
 130        {
 16131            AuditResidueLifecycleEvent appended = await lifecycleStore.AppendAsync(
 16132                lifecycleEvent,
 16133                cancellationToken).ConfigureAwait(false);
 134
 12135            return new NcatAuditCompletionDeliveryResult(
 12136                NcatAuditCompletionDeliveryDisposition.Delivered,
 12137                "lifecycle-event-appended",
 12138                appended.EventId,
 12139                receipt,
 12140                appended);
 141        }
 0142        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 143        {
 0144            throw;
 145        }
 4146        catch (Exception exception)
 147        {
 4148            return PersistenceFailure(handoff, lifecycleEventId, receipt, lifecycleEvent, exception);
 149        }
 30150    }
 151
 152    private static NcatAuditCompletionDeliveryResult? ValidateHandoff(NcatAuditCompletionHandoff handoff)
 153    {
 30154        return string.IsNullOrWhiteSpace(handoff.CompletionEntryId)
 30155            ? Terminal("completion-entry-id-required")
 30156            : string.IsNullOrWhiteSpace(handoff.OperationExecutionId)
 30157            ? Terminal("operation-execution-id-required")
 30158            : string.IsNullOrWhiteSpace(handoff.DecisionAuditRecordId)
 30159            ? Terminal("decision-audit-record-id-required")
 30160            : string.IsNullOrWhiteSpace(handoff.PersistenceOutcome)
 30161            ? Terminal("persistence-outcome-required")
 30162            : handoff.AuditRecordCount < 0
 30163            ? Terminal("audit-record-count-invalid")
 30164            : handoff.DeliveryAttempt < 0
 30165            ? Terminal("delivery-attempt-invalid")
 30166            : null;
 167    }
 168
 169    private static NcatAuditCompletionDeliveryResult? ValidateDecisionCorrelation(
 170        NcatAuditCompletionHandoff handoff,
 171        IAsiBackboneAuditResidue residue)
 172    {
 22173        string? correlationId = NormalizeOptional(handoff.CorrelationId);
 22174        if (correlationId is not null &&
 22175            !string.Equals(correlationId, residue.CorrelationId, StringComparison.Ordinal))
 176        {
 2177            return Terminal("correlation-id-mismatch");
 178        }
 179
 20180        string? traceId = NormalizeOptional(handoff.TraceId);
 20181        return traceId is not null &&
 20182            !string.Equals(traceId, residue.TraceId, StringComparison.Ordinal)
 20183            ? Terminal("trace-id-mismatch")
 20184            : null;
 185    }
 186
 187    private NcatAuditCompletionDeliveryResult PersistenceFailure(
 188        NcatAuditCompletionHandoff handoff,
 189        string lifecycleEventId,
 190        GovernedOperationExecutionReceipt receipt,
 191        AuditResidueLifecycleEvent lifecycleEvent,
 192        Exception exception)
 193    {
 4194        bool deadLettered = options.DeadLetterAfterAttempts is int threshold &&
 4195            handoff.DeliveryAttempt >= threshold;
 196
 4197        return new NcatAuditCompletionDeliveryResult(
 4198            deadLettered
 4199                ? NcatAuditCompletionDeliveryDisposition.DeadLetter
 4200                : NcatAuditCompletionDeliveryDisposition.Retryable,
 4201            deadLettered
 4202                ? "lifecycle-persistence-dead-lettered"
 4203                : "lifecycle-persistence-failed",
 4204            lifecycleEventId,
 4205            receipt,
 4206            lifecycleEvent,
 4207            exception.GetType().Name);
 208    }
 209
 210    private static bool TryMapOutcome(
 211        string sourceOutcome,
 212        out GovernedOperationPersistenceOutcome persistenceOutcome)
 213    {
 24214        string normalized = string.Concat(sourceOutcome
 24215            .Where(char.IsLetterOrDigit)
 24216            .Select(char.ToLowerInvariant));
 217
 24218        persistenceOutcome = normalized switch
 24219        {
 16220            "committed" => GovernedOperationPersistenceOutcome.Committed,
 2221            "failed" => GovernedOperationPersistenceOutcome.Failed,
 2222            "rolledback" => GovernedOperationPersistenceOutcome.RolledBack,
 4223            "completedwithoutmutation" or "nomutation" => GovernedOperationPersistenceOutcome.CompletedWithoutMutation,
 0224            _ => default
 24225        };
 226
 24227        return normalized is "committed" or "failed" or "rolledback" or "completedwithoutmutation" or "nomutation";
 228    }
 229
 230    private static string CreateLifecycleEventId(string completionEntryId)
 231    {
 18232        byte[] digest = SHA256.HashData(Encoding.UTF8.GetBytes(completionEntryId.Trim()));
 18233        return string.Concat(LifecycleEventPrefix, Convert.ToHexString(digest).ToLowerInvariant());
 234    }
 235
 236    private static bool Equivalent(
 237        AuditResidueLifecycleEvent existing,
 238        AuditResidueLifecycleEvent candidate)
 239    {
 2240        if (existing.Stage != candidate.Stage ||
 2241            !string.Equals(existing.CorrelationId, candidate.CorrelationId, StringComparison.Ordinal) ||
 2242            !string.Equals(existing.AuditResidueId, candidate.AuditResidueId, 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
 20263        return keys.All(key => string.Equals(
 20264            GetMetadata(existing.Metadata, key),
 20265            GetMetadata(candidate.Metadata, key),
 20266            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    {
 66276        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    {
 10284        return new NcatAuditCompletionDeliveryResult(
 10285            NcatAuditCompletionDeliveryDisposition.Terminal,
 10286            reasonCode,
 10287            lifecycleEventId,
 10288            FailureType: failureType);
 289    }
 290}