Optional NCAT Audit-Completion Adapter
The samples/NcatAuditCompletionAdapter project is a reference integration that maps an NCAT mutation-audit completion into AsiBackbone governed execution lifecycle evidence.
The adapter is intentionally outside every required AsiBackbone package. It references AsiBackbone.Core, but AsiBackbone.Core does not reference NCAT. NCAT likewise remains independently installable and does not require AsiBackbone assemblies, configuration, migrations, or services.
Authority boundary
| Component | Authoritative responsibility |
|---|---|
| NCAT | Application mutation details, transaction outcome, mutation batch, privacy-safe canonical manifest, and completion-outbox delivery state |
| AsiBackbone | Policy decision evidence, governed execution receipt, lifecycle evidence, optional signing, and outbox delivery |
| Optional adapter | Translation, correlation validation, idempotent lifecycle append, and normalized handoff result |
The adapter does not create a distributed transaction and does not claim exactly-once delivery. It uses an at-least-once handoff with a deterministic lifecycle event identifier so duplicate attempts can be detected safely.
Independent installation
An application may use either product without the other:
NCAT host only
-> NCAT mutation records and completion receipt/outbox
AsiBackbone host only
-> policy decisions and governed lifecycle records
Combined host
-> optional translation at the application composition boundary
The reference sample uses a source-neutral NcatAuditCompletionHandoff instead of a compile-time NCAT type. A combined host translates its current NCAT completion receipt or completion-outbox entry into this minimized contract.
Required handoff fields
Every handoff requires:
- a stable NCAT completion entry identifier used as the idempotency key;
- the logical operation execution identifier;
- the persisted AsiBackbone decision audit record identifier;
- the NCAT persistence outcome;
- the completion timestamp.
Correlation ID, trace ID, and execution attempt ID should be supplied whenever available. The adapter rejects a supplied correlation or trace identifier that conflicts with the resolved decision receipt.
A committed outcome additionally requires:
- mutation batch identifier;
- audit record count greater than zero;
- canonical privacy-safe mutation manifest hash;
- manifest hash algorithm.
Failed, rolled-back, and completed-without-mutation outcomes cannot claim committed mutation evidence.
Outcome mapping
| NCAT handoff outcome | AsiBackbone outcome |
|---|---|
committed |
Committed |
failed |
Failed |
rolled-back |
RolledBack |
no-mutation or completed-without-mutation |
CompletedWithoutMutation |
Outcome matching ignores case and separators. Unknown outcomes are terminal validation failures rather than guessed mappings.
Combined host registration
The host supplies a decision-receipt resolver and a durable lifecycle store:
var adapter = new NcatAuditCompletionAdapter(
lifecycleStore,
decisionReceiptResolver,
new NcatAuditCompletionAdapterOptions
{
PersistenceProvider = "NCAT",
DeadLetterAfterAttempts = 10
});
INcatDecisionReceiptResolver is host-owned because the decision receipt may be stored through EF Core, an in-memory development provider, or another repository.
A completion dispatcher can translate the source entry and invoke the adapter:
var handoff = new NcatAuditCompletionHandoff(
CompletionEntryId: completionEntry.Id,
PersistenceOutcome: completionEntry.Receipt.PersistenceOutcome,
CompletedUtc: completionEntry.Receipt.CompletedUtc,
OperationExecutionId: completionEntry.Receipt.OperationExecutionId,
ExecutionAttemptId: completionEntry.Receipt.ExecutionAttemptId,
DecisionAuditRecordId: completionEntry.Receipt.DecisionAuditRecordId,
CorrelationId: completionEntry.Receipt.CorrelationId,
TraceId: completionEntry.Receipt.TraceId,
MutationBatchId: completionEntry.Receipt.MutationBatchId,
AuditRecordCount: completionEntry.Receipt.AuditRecordCount,
MutationManifestHash: completionEntry.Receipt.MutationManifestHash,
MutationManifestAlgorithm: completionEntry.Receipt.MutationManifestAlgorithm,
DeliveryAttempt: completionEntry.AttemptCount);
NcatAuditCompletionDeliveryResult result =
await adapter.DeliverAsync(handoff, cancellationToken);
if (result.ShouldAcknowledgeSource)
{
await completionOutbox.MarkDeliveredAsync(completionEntry.Id, cancellationToken);
}
else
{
await completionOutbox.RecordAttemptAsync(
completionEntry.Id,
result.Disposition.ToString(),
result.ReasonCode,
cancellationToken);
}
The source entry is acknowledged only after the lifecycle event has been durably appended, or when an equivalent lifecycle event already exists.
A host that receives NCAT's ApplicationAuditCompletionMessage through an IApplicationAuditCompletionPublisher can instead model it as NcatAuditCompletionMessage and validate it before translation:
if (!NcatAuditCompletionContract.TryCreateHandoff(
message,
deliveryAttempt,
out NcatAuditCompletionHandoff? handoff,
out string? reasonCode))
{
// Contract violation: report a terminal publish failure to NCAT with reasonCode.
}
TryCreateHandoff applies version 1 of NCAT's audit-completion contract and uses the message idempotency key as the completion entry identifier. Hosts that retain the canonical manifest text can also call TryVerifyCanonicalManifest to confirm that the manifest is a single JSON value and to confirm the batch, record count, schema version, and SHA-256 digest. The contract rules are described in NCAT contract vectors.
Delivery dispositions
| Disposition | Meaning | Acknowledge NCAT source entry? |
|---|---|---|
Delivered |
Lifecycle event appended | Yes |
Duplicate |
Equivalent event already exists | Yes |
Retryable |
Append failed and should be retried | No |
Deferred |
Required decision receipt is not available yet | No |
Terminal |
Invalid handoff or idempotency conflict | No |
DeadLetter |
Configured retry threshold was exhausted | No |
Dead-letter classification does not delete local evidence or claim successful delivery. The host remains responsible for retaining and reconciling the NCAT completion entry.
Idempotency and conflict handling
The lifecycle event identifier is deterministically derived from the NCAT completion entry identifier. On a retry:
- the adapter looks up the event before appending;
- an equivalent event returns
Duplicate; - a different event under the same idempotency key returns
Terminalwithidempotency-conflict; - append failure remains retryable or dead-lettered according to host configuration.
This protects against duplicate publication while avoiding an exactly-once claim.
Metadata minimization
The adapter carries only opaque identifiers, counts, hashes, outcome, provider label, and the source completion entry identifier. It does not accept or copy:
- entity keys;
- original or current values;
- request bodies;
- secrets or credentials;
- unrestricted exception messages.
Failure results expose only the exception type name. Detailed diagnostics should remain in the host's protected local logs.
NCAT contract vectors
The adapter models NCAT independently, so both repositories could drift while their own tests stay green. To prevent that, the sample tests replay NCAT's published, machine-readable audit-completion contract vectors. Neither product takes a compile-time dependency on the other.
The vectors are vendored from a pinned NCAT commit:
| File | Purpose |
|---|---|
tests/AsiBackbone.Samples.NcatAuditCompletionAdapter.Tests/ContractVectors/ncat/v1/audit-completion-vectors.json |
Exact bytes of NCAT's contracts/audit-completion/v1/audit-completion-vectors.json at the pinned revision |
tests/AsiBackbone.Samples.NcatAuditCompletionAdapter.Tests/ContractVectors/ncat/ncat-contract-pin.json |
NCAT repository, pinned commit SHA, source path, and SHA-256 of the vendored file |
For the AsiBackbone 7.0.0 release candidate, the reviewed commit pin
8aec7d32438907e73b09f283d4f61221a6b0cedc remains the immutable release
evidence. The vendored bytes match both NCAT main and the NCAT 2.11.0
release-candidate commit 2f16c2aedf0689fce78f756e614d0278fb0c8597.
Keeping the contract-producing commit, rather than replacing it with a tag that
does not yet exist during release preparation, preserves exact provenance. A
later metadata-only repin to v2.11.0 is unnecessary unless the published tag
contains different vector bytes, which the drift check would reject.
NcatContractVectorTests checks that:
- the vendored file matches the pinned SHA-256;
- the contract version, schema versions, digest algorithm, idempotency prefix, and supported outcomes match
NcatAuditCompletionContract; - each valid vector's canonical manifest bytes reproduce its expected digest and its idempotency key is recomputed from the destination and the trimmed mutation batch ID;
- each valid vector's receipt and message agree, pass
TryCreateHandoff, and deliver with the operation, attempt, decision, correlation, trace, batch, count, algorithm, digest, and timestamp preserved. Vectors without an operation or decision identifier are rejected with the adapter's documented reason codes; - each published invalid message is rejected with a specific reason code;
- a lowercase manifest digest is rejected by both
TryCreateHandoffandTryVerifyCanonicalManifestwithinvalid-manifest-hash, because the contract encodes digests as uppercase hex; - the no-mutation, failed, and rolled-back scenarios cannot carry committed batch evidence.
Valid vectors, invalid messages, and no-message scenarios are replayed from the reviewed names in NcatContractVectorTests, not discovered from the fixture. A test fails with review guidance when NCAT publishes or removes any of these names, or publishes an outcome or contract major version the adapter does not recognize.
Drift reporting
The NCAT Contract Vectors workflow runs weekly, on demand, and on pull requests that change the vendored vectors. scripts/Test-NcatContractVectorPin.ps1 confirms that the vendored bytes still match the pinned NCAT revision. It then compares them with NCAT main and fails with a summary when NCAT publishes different vectors, removes them, or adds a newer contract major version. If it cannot list NCAT's contract versions, it reports that as a finding rather than passing on an incomplete check. The weekly failure is the signal that the pin needs review; it does not block unrelated pull requests. Pull request runs execute the contributor's copy of the script, so they call the public NCAT endpoints without a GitHub token; only scheduled and manual runs, which maintainers trigger, pass one, to raise the API rate limit.
Updating the pinned vectors
- Read the NCAT change and its compatibility classification in NCAT's contract README.
- Copy
contracts/audit-completion/v1/audit-completion-vectors.jsonfrom the new NCAT commit or release tag without modifying it..gitattributeskeeps the vendored JSON as LF, so the bytes match upstream. - Update
revisionandsha256inncat-contract-pin.json. Use the full commit SHA. - Run the sample tests. Extend
NcatAuditCompletionContractandNcatContractVectorTestsfor any new vector, reason, outcome, or schema version. Add a reviewed vector name to the supported-vector allowlist only after the adapter handles it. - For a new contract major version, vendor the new
vN/directory alongside the existing one until the adapter supports it. - Run
./scripts/Test-NcatContractVectorPin.ps1and confirm it reports no drift.
Related work
- AsiBackbone issue #634 introduced the framework-neutral governed execution receipt and lifecycle helpers.
- NCAT issue #367 owns the privacy-safe canonical mutation manifest and hash.
- NCAT issues #368 through #370 own transaction coordination, completion outbox/dispatch, reconciliation, health checks, and metrics.
- NCAT issue #580 publishes the versioned audit-completion contract vectors, and AsiBackbone issue #826 replays them against this adapter.