The Compliance Harness
On 30 April 2026, a refactoring commit during the framework’s own extraction out of its first host application began shipping empty Affidavits: every proposed write carried fields tagged ProvenanceSource.Empty and no real values. The entire test suite at the time — 330 of 330 tests — stayed green, because those tests asserted the shape of an Affidavit (the right field names, the right count, Fields.Length > 0) rather than the substance of its provenance (whether a field claiming a value actually swore to where that value came from). The regression surfaced only when a real user typed a real message and the review card came back blank.
The lesson the framework now enforces mechanically: a test suite can be 100% green and 0% truthful if it asserts shape, not meaning. Affiant.Testing.ComplianceHarness exists so that a host’s own inference strategies — the code that decides what an Affidavit’s fields actually contain, per Affidavits & Provenance — can’t fall into the same trap. It ships as a normal test-project package (Affiant.Testing.ComplianceHarness, depending only on Affiant.Core; see Installation and Packages) with one static entry point, ComplianceHarness.Verify(IServiceCollection), and one public, reusable check, ComplianceHarness.AssertProvenanceIsSubstantive, that Verify runs against every fixture case by default.
What Verify checks
Section titled “What Verify checks”Verify takes the IServiceCollection your test project has assembled, builds a service provider from it, and does two things in sequence.
Discoverability. It reads every AffiantToolDescriptor registered in IAffiantToolRegistry — the registry AddAffiantTool<TStrategy>() populates, covered in Authoring Write Tools — and filters to the ones that describe a write operation (Operation.Kind of "WriteCreate" or "WriteUpdate") with a non-null InferenceStrategy. For each one, it checks whether an ITaskInferenceComplianceFixture is registered whose Strategy property matches. Any write strategy without a paired fixture becomes a MissingFixture.
Case execution and substance gating. For every fixture that is paired to a registered write strategy, Verify runs each of the fixture’s InferenceFixtureCase entries: it drives the strategy through the same inference-and-projection path a real tool call takes, records whether the fixture’s own hand-written Assertion passed, and — independently of that assertion — runs AssertProvenanceIsSubstantive against the resulting Affidavit.
The result is a ComplianceVerificationResult:
public sealed record ComplianceVerificationResult( bool Passed, IReadOnlyList<MissingFixture> MissingFixtures, IReadOnlyList<FixtureFailure> FixtureFailures, IReadOnlyList<SubstanceFailure> SubstanceFailures);Passed is true only when all three lists are empty, and the three are deliberately orthogonal:
MissingFixtures— a write strategy with no paired fixture at all (discoverability).FixtureFailures— a fixture’s ownAssertionreturnedfalseor threw, or the case couldn’t run (missing test double, unregistered strategy, an exception mid-inference).SubstanceFailures— the harness’s own gate found a hollow Affidavit, regardless of what the fixture author chose to assert. This is the direct, executable guard against the empty-Affidavit regression class.
A fixture author who writes a narrow assertion — affidavit.Fields.Length > 0, say, which would have passed against an all-Empty Affidavit just as easily as a real one — does not get to silently skip the substance check. SubstanceFailures runs whether or not the fixture’s own Assertion would have caught the same problem.
Writing a fixture
Section titled “Writing a fixture”A fixture implements ITaskInferenceComplianceFixture, from Affiant.Abstractions.Interfaces:
public interface ITaskInferenceComplianceFixture{ Type Strategy { get; } IEnumerable<InferenceFixtureCase> Cases { get; }}Strategy names the ITaskInferenceStrategy implementation this fixture verifies — it’s how Verify pairs a fixture to a write strategy’s registered AffiantToolDescriptor.InferenceStrategy. Cases is a sequence of InferenceFixtureCase records:
public sealed record InferenceFixtureCase( string Name, ChatHistory History, IReadOnlyDictionary<string, object?> Arguments, Func<Affidavit, bool> Assertion);History is the conversation the strategy will infer against, Arguments are the tool call’s current arguments (captured the same way ToolArgumentCaptureFilter would — see Context Fabric), and Assertion is your own predicate over the produced Affidavit. Continuing the LeaveTaskInferenceStrategy from Quickstart — the strategy behind the request_leave tool, with StartDate, EndDate, LeaveType, and Reason fields — a fixture for it looks like this:
using Affiant.Abstractions.Interfaces;using Affiant.Abstractions.Models;using Microsoft.SemanticKernel.ChatCompletion;
public sealed class LeaveComplianceFixture : ITaskInferenceComplianceFixture{ public Type Strategy => typeof(LeaveTaskInferenceStrategy);
public IEnumerable<InferenceFixtureCase> Cases { get { var history = new ChatHistory(); history.AddUserMessage( "I need annual leave from 2026-08-03 to 2026-08-07, I'll be at a family event.");
yield return new InferenceFixtureCase( Name: "happy_path_annual_leave", History: history, Arguments: new Dictionary<string, object?> { ["startDate"] = "2026-08-03", ["endDate"] = "2026-08-07", ["leaveType"] = "Annual", ["reason"] = "Family event", }, Assertion: affidavit => affidavit.Fields.Single(f => f.Name == "LeaveType").Value as string == "Annual"); } }}The Assertion above checks one domain fact your own test cares about. It says nothing about provenance — that’s not a gap you need to fill by hand; it’s exactly what AssertProvenanceIsSubstantive checks independently, below.
Verify needs a way to turn History and Arguments into structured-output JSON without calling a real LLM. Register a test double for IInferenceCompletionPort (Affiant.Abstractions.Interfaces) that returns canned JsonElement output matching the shape TaskInferenceStep expects — an object keyed by field name, each with a value and a confidence:
using System.Text.Json;using Affiant.Abstractions.Interfaces;using Affiant.Abstractions.Models;
public sealed class RecordedInferencePort(string json) : IInferenceCompletionPort{ public Task<JsonElement> CompleteStructuredAsync( InferenceCompletionRequest request, CancellationToken cancellationToken = default) => Task.FromResult(JsonDocument.Parse(json).RootElement.Clone());}const string HappyPathJson = """ { "StartDate": { "value": "2026-08-03", "confidence": 0.95 }, "EndDate": { "value": "2026-08-07", "confidence": 0.95 }, "LeaveType": { "value": "Annual", "confidence": 0.90 }, "Reason": { "value": "Family event", "confidence": 0.85 } } """;Running Verify
Section titled “Running Verify”Wire the strategy, the fixture, and the recorded port into an IServiceCollection, then call ComplianceHarness.Verify:
var services = new ServiceCollection() .AddAffiantCore() .AddSingleton<IInferenceCompletionPort>(new RecordedInferencePort(HappyPathJson)) .AddAffiantTool<LeaveTaskInferenceStrategy>( functionName: "request_leave", operation: Operation.WriteCreate, entityType: "LeaveRequest") .AddSingleton<ITaskInferenceComplianceFixture>(new LeaveComplianceFixture());
var result = ComplianceHarness.Verify(services);
Assert.True(result.Passed, $"Compliance check failed.\n" + $"Missing fixtures: {string.Join(", ", result.MissingFixtures)}\n" + $"Fixture failures: {string.Join(", ", result.FixtureFailures)}\n" + $"Substance failures: {string.Join(", ", result.SubstanceFailures)}");AddAffiantCore() is required — it registers IAffiantToolRegistry and IObservabilityEventStream<AffidavitEmittedEvent>, both of which Verify resolves directly. Worth knowing precisely what Verify does not need from the container: for each case, it constructs its own ContextFabric, TaskInferenceStep, and TaskInferenceRunner directly — one fresh, isolated ContextFabric per case, so cases never leak state into each other — rather than resolving them from DI. It then builds a SchemaDrivenAffidavitProjection from your strategy instance, whatever IDeterministicFieldSource implementations you’ve registered, and the same IObservabilityEventStream<AffidavitEmittedEvent> AddAffiantCore() provides, and projects the Affidavit through it — the same projection path a real write tool call takes, described in Affidavits & Provenance. What it does need beyond AddAffiantCore(): your ITaskInferenceStrategy registered (AddAffiantTool<TStrategy>, which registers both the strategy and its AffiantToolDescriptor atomically), an IInferenceCompletionPort test double, and each ITaskInferenceComplianceFixture.
Discoverability has no lenient mode
Section titled “Discoverability has no lenient mode”The missing-fixture check is unconditional — there’s no flag on Verify to allowlist a strategy or soften the check for a subset of tools. Every AffiantToolDescriptor your registry holds for a WriteCreate/WriteUpdate operation with a non-null InferenceStrategy is checked, every time. If you register ten write tools and pair fixtures to nine, Verify reports one MissingFixture naming the tenth strategy’s type and function name — it does not pass by default and let you opt in to strictness later.
One boundary worth knowing: the check only considers descriptors that carry a non-null InferenceStrategy. AddAffiantTool<TStrategy>() always sets one, so the ordinary registration path is fully covered. A descriptor constructed and registered directly against IAffiantToolRegistry — bypassing AddAffiantTool<TStrategy> — with a null InferenceStrategy on a write operation is invisible to this check, since there is no strategy type to pair a fixture to in the first place.
FixtureFailures: when a case can’t run, or your assertion says no
Section titled “FixtureFailures: when a case can’t run, or your assertion says no”A FixtureFailure (StrategyType, FixtureCaseName, Reason) is your fixture’s own concern. Verify records one when:
- No
IInferenceCompletionPortis registered at all — the case can’t run, and this is reported as a failure rather than thrown as an exception. - The fixture’s
Strategytype doesn’t resolve from the container as anITaskInferenceStrategy— usually a missing or mistypedAddAffiantTool<TStrategy>()call. - An exception occurs while running the case through
TaskInferenceRunner.RunAsync— unwrapped fromAggregateExceptionwhere necessary, so the reported reason names the real underlying exception type and message, not a wrapper. - The case’s own
Assertionthrows. - The case’s own
Assertionruns cleanly and returnsfalse.
These are exactly the failures a fixture author would expect from ordinary test-writing — a broken test double, a typo’d strategy registration, an assertion that doesn’t hold. None of them says anything about provenance quality; that’s the next section.
The substance gate: AssertProvenanceIsSubstantive
Section titled “The substance gate: AssertProvenanceIsSubstantive”AssertProvenanceIsSubstantive(ITaskInferenceStrategy strategy, string fixtureCaseName, Affidavit affidavit) is public, static, and runs automatically inside Verify for every case — but it’s also usable standalone, so the identical check can be reused by other tooling without duplicating the logic. It returns a list of SubstanceFailure records:
public sealed record SubstanceFailure( Type StrategyType, string FixtureCaseName, string FieldName, string Reason);FieldName names the specific field a check failed on; for a violation that isn’t scoped to one field, it carries a marker like "(affidavit)" instead. The method runs four checks, in order, against a single produced Affidavit:
1. Affidavit.Fields is non-empty. An empty Fields array is the empty-Affidavit regression in its most literal form — no sworn fields were produced at all. This check short-circuits: if it fails, the method returns immediately with a single SubstanceFailure and skips the per-field checks below, since there are no fields to check.
2. Every field carries a provenance chain. For each AffidavitField, if Provenance is null or Provenance.Current is null, that’s a failure — a field emitted with no provenance at all is indistinguishable from “the framework forgot to track it,” which is exactly what Rule 7 forbids.
3. A populated value must be sworn. If a field carries a real value (non-null, and — for strings — non-empty and non-whitespace) but its current ProvenanceTag.Source is ProvenanceSource.Empty, that’s the hollow signature itself: a field asserting a value while swearing nothing about where it came from. This is the check that would have caught the empty-Affidavit regression directly.
4. Required fields project to mandatory fields. If the strategy declared a TaskInferenceField with Required = true, the corresponding AffidavitField.IsMandatory must be true. A strategy that silently drops a field’s required status on the way to the Affidavit fails here.
The per-fixture invariant: prove the strategy can produce substance
Section titled “The per-fixture invariant: prove the strategy can produce substance”The four checks above run per field, per case. Verify adds one more check at the fixture level, generalizing a narrower guard the framework once wrote for a single strategy after the April 2026 empty-Affidavit audit into a rule for every registered fixture: across all of a fixture’s cases that produced an Affidavit at all, at least one must be substantive — meaning at least one field’s current provenance source is not ProvenanceSource.Empty. A fixture whose every case yields an all-Empty Affidavit fails with a SubstanceFailure naming the fixture’s strategy type, "(all cases)" as the case name, and "(affidavit)" as the field name, even if every individual case passed checks 1–4 above (an all-Empty Affidavit with Fields.Length > 0 and every field carrying an explicit Empty tag is, technically, fully “tagged” — just never demonstrating that the strategy can produce a real value). This check is only raised when at least one case actually produced an Affidavit; a fixture whose every case already failed outright (recorded in FixtureFailures) doesn’t also get this failure piled on.
What the gate deliberately does not check
Section titled “What the gate deliberately does not check”Two things the substance gate stops short of, on purpose. It does not require a Required = true field to actually carry a populated value — an empty mandatory field, correctly tagged ProvenanceSource.Empty, is a legitimate outcome the gate lets through; deciding whether that’s acceptable to approve is a reviewer-UI concern (the Evidence Card renders IsMandatory and Empty together as a flag), not a projection-truthfulness concern the harness should adjudicate. And it does not forbid ProvenanceSource.Empty outright across a case: a case that legitimately has no conversational basis to infer anything is allowed to yield an all-Empty Affidavit and pass the per-field checks cleanly — the “a strategy must produce substance somewhere” rule is enforced once, at the fixture level, not by banning Empty from any single case.
Why Verify runs the gate by default
Section titled “Why Verify runs the gate by default”There’s no parameter on Verify to disable AssertProvenanceIsSubstantive — it isn’t opt-in, and a fixture author cannot satisfy ComplianceHarness.Verify by writing a lenient Assertion and skipping the substance question. That’s the whole point: the regression that motivated this package passed 330 hand-written assertions cleanly, because none of them asked whether an Affidavit’s provenance was real. The gate exists precisely so that question gets asked automatically, on every case, for every registered write strategy, whether or not the person writing the fixture thought to ask it.
Where this fits
Section titled “Where this fits”Affidavits & Provenance covers the Affidavit, ProvenanceTag, and ProvenanceChain shapes this page checks against, and the seven-source determinism hierarchy that ProvenanceSource.Empty sits at the bottom of. The Seven Normative Rules states Rule 7 — the invariant the substance gate exists to enforce — in full. Authoring Write Tools covers [AffiantWriteTool], ITaskInferenceStrategy, and AddAffiantTool<TStrategy>() in depth; a fixture only makes sense once a write tool and its strategy exist. Packages has the full dependency graph, including where Affiant.Testing.ComplianceHarness sits relative to Affiant.Core.