Skip to content

Affidavits & Provenance

An affiant is one who swears to the truth of a written statement. Affiant borrows the metaphor directly: every AI-proposed database mutation becomes an Affidavit — a sworn statement, one field at a time, about where each value came from and how much the framework trusts it.

This page covers the two types that make that possible — Affidavit and ProvenanceTag — and the rule that makes them trustworthy: provenance is never optional, never omitted, and never invented.

Affidavit is defined in Affiant.Abstractions.Models and is the core write-side contract of the framework. The record header below is complete; the record’s two methods, Create and WithFields, are omitted here and described under the bullets. One name in it lives elsewhere: AffiantProtocol, whose Version constant is the last parameter’s default, is declared one namespace up in Affiant.Abstractions, so a file that reproduces the header needs both usings:

public sealed record Affidavit(
string OperationType,
string EntityType,
string? EntityId,
AffidavitField[] Fields,
float AggregateConfidence,
float? PopulatedConfidence,
int EmptyFieldCount,
string[] Warnings,
bool RequiresConfirmation,
int? ConversationTurn = null,
DateTimeOffset? CreatedAt = null,
string ProtocolVersion = AffiantProtocol.Version);
  • OperationType — the shape of the operation rather than the host’s own label for it. Operation.IsUpdateShaped recognises exactly two spellings as update-shaped, case-insensitively: "WriteUpdate" and the bare "update". Everything else — "WriteCreate", "WriteDelete", and a host verb such as "UpdateCustomer" — is create-shaped, and the shape is load-bearing: SchemaDrivenAffidavitProjection.Project throws ArgumentException when an entity id is passed with a create-shaped operation (and when one is missing from an update-shaped one), IPreviousValueSource is consulted for update-shaped operations only, so PreviousValue stays null otherwise, and the canonical bytes an execution grant binds to carry "create" in place of the host’s verb. A host’s own verb travels beside the shape, never instead of it: the Evidence Card envelope’s HostOperation is where it goes, so a reviewer surface can head the card with the term a person recognises while a policy still tests the protocol’s two-valued shape vocabulary.
  • EntityType — the domain entity being mutated, e.g. "WorkOrder".
  • EntityId — the primary key of the entity being changed. It is non-null if and only if OperationType is update-shaped (protocol rule AF-3): null for a create, where no identity exists yet, and named on an update. The projection refuses both violations of that biconditional rather than guessing which was meant.
  • Fields — every field the mutation touches, each one an AffidavitField (below). This array is never empty for a real proposal — an Affidavit with zero fields swears to nothing.
  • AggregateConfidence — the minimum over every proposed field, with an Empty field counting as 0 whatever its own tag says. It is 0 when some proposed field has unknown provenance. A single low-confidence or unsourced field drags the whole Affidavit down; the framework never lets one weak field hide behind a strong average.
  • PopulatedConfidence (float?) — the minimum confidence over the proposed fields whose current tag is not Empty, null rather than 0 when there are none. The partition is by grade rather than by whether a value is present: a valueless field carrying, say, an Inferred tag counts toward this number, and a field holding a value under an Empty tag does not. This is what makes AggregateConfidence reading 0 readable at all: “there is nothing populated to be confident about” and “the populated fields are worthless” are different statements, and AggregateConfidence alone can’t tell them apart.
  • EmptyFieldCount (int) — how many proposed fields carry an Empty current tag.
  • Warnings — business-rule violations detected while assembling the proposal, surfaced to the reviewer alongside the fields. The host’s own sentences are not the only ones a filed record carries: ReviewGate appends up to three of its own as it files — the reason a policy gave for degrading its verdict, the sentence a tool the host has declared uncovered puts on the entry, and the sentence a ReferralRequired or MultiParty requirement puts on it — and puts them on the Docket row as well as the card, so a blocked entry is never shown with no decision available and no explanation. Nothing else writes here: a failure inside inference is absorbed by TaskInferenceRunner, which records an inference.failed telemetry event and returns an empty result, so the unmerged fields swear Empty; a cancellation is the one arm it does not absorb — it records the same event with error kind cancelled and rethrows. No warning lands on the record on any of the three paths.
  • RequiresConfirmation — whether this Affidavit says a human reviewer is needed at all. Nothing rewrites the record’s own flag. What travels to a reviewer surface is the Evidence Card envelope’s RequiresConfirmation, which EvidenceCardRequest.For seeds from blocked is null && affidavit.RequiresConfirmation — a blocked entry never claims a confirmation is being awaited — and which the gate overrides where the verdict says so: on a Standing Order approval ReviewGate builds the card with requiresConfirmation: false, because no person was asked. The auto-approved case is therefore the gate’s stamp on the card, driven by the verdict a host’s IApprovalPolicy returned, not a rewrite of the sworn record — see Review Gate & Write Executors.
  • ConversationTurn, CreatedAt, ProtocolVersion — the three properties no strategy’s field schema declares, because none of them is a field of the write: the conversation turn the proposal was made on (null when it didn’t come from one — SchemaDrivenAffidavitProjection leaves it unset, and a caller that knows the turn passes it to Affidavit.Create), when the record was built, and which protocol version the record speaks (a non-nullable string, defaulting to the build’s own AffiantProtocol.Version). A caller doesn’t have to stamp CreatedAt itself — ReviewGate stamps it with its own clock as it files, the same instant a Docket entry’s deadline is measured from, so a record that arrives unstamped is stamped once and a record that arrives already stamped keeps what it says. All three are among the ten properties the Affiant protocol’s own Affidavit schema requires; Warnings and RequiresConfirmation are the two properties of this record that the schema does not carry at all — they travel on the Evidence Card envelope, and CanonicalSerializer drops them from the canonical form.

AggregateConfidence, PopulatedConfidence, and EmptyFieldCount are the three numbers AffidavitConfidence.Compute(fields) derives from a field list in one place. Reach for Affidavit.Create(operationType, entityType, entityId, fields, warnings, requiresConfirmation, conversationTurn, createdAt) rather than the raw constructor — the last three parameters are optional, and it computes all three numbers from the fields it’s given, so a hand-written aggregate can never disagree with what it’s meant to summarise — and affidavit.WithFields(...) to recompute them after a field changes (an accepted amendment, for instance). conversationTurn and createdAt are how Create carries the two stamps the bullet above describes; they are not the only way to set them. The primary constructor is public and takes both, and a with expression sets either one — which is exactly what ReviewGate does to a record that arrives unstamped (context.Affidavit with { CreatedAt = now }).

An Affidavit never causes a write by itself — see the Seven Normative Rules (Rule 3) and Tool Envelopes for how a WriteProposal carrying an Affidavit goes through the review flow, and is approved there by a human reviewer or by a Standing Order a human authored in advance, before any row changes.

Each entry in Fields is an AffidavitField:

public sealed record AffidavitField(
string Name,
object? Value,
object? PreviousValue,
ProvenanceChain Provenance,
bool IsMandatory = false,
string Kind = AffidavitFieldKind.Text,
IReadOnlyList<string>? AllowedValues = null,
string? Pattern = null);
  • Name — the domain field name, e.g. "priority" or "customerEmail".
  • Value — the proposed new value.
  • PreviousValue — the current value, or null for a create operation where there is nothing to compare against.
  • Provenance — the full ProvenanceChain for this field, described below.
  • IsMandatory — whether this field is required for the operation to make domain sense.
  • Kind — the reviewer-UI rendering hint for this field’s value: one of the AffidavitFieldKind string constants ("text", "number", "date", "enum"). A plain string rather than an enum deliberately, so a producer and a consumer on different transports never need to agree on a shared enum type.
  • AllowedValues — the closed set a reviewer may pick from when Kind is "enum"; null otherwise.
  • Pattern — an optional validation regex the value is expected to satisfy, forwarded from the originating TaskInferenceField.Pattern when present. The framework carries it and never applies it: the three inference completion ports write it into the structured-output prompt, and FieldPresentation.For lifts it onto the Evidence Card envelope’s presentation hints, whose own remarks say a Pattern is “carried verbatim and never compiled or applied by the gate”. Nothing in the framework compiles it or refuses a value against it; a host that wants a value refused enforces that in its own policy.

IsMandatory is not decorative. When a host implements ITaskInferenceStrategy to drive structured-output inference (see Tool Envelopes), it declares its field schema as a list of TaskInferenceField records:

public record TaskInferenceField(
string Name,
string JsonType,
string Description,
int? MaxLength = null,
string? Pattern = null,
IReadOnlyList<string>? Enum = null,
bool Required = false,
string? Format = null,
bool Projected = true);

Every TaskInferenceField declared with Required = true must project to an AffidavitField with IsMandatory == true in the resulting Affidavit. Affiant’s compliance harness — see The Compliance Harness — checks this projection mechanically, so a strategy cannot silently drop a required field’s mandatory status on the way to the Evidence Card.

What IsMandatory does not do: it does not force a value to be present, and it does not stop a human reviewer approving. A mandatory field can legitimately arrive at review with Value unset and provenance ProvenanceSource.Empty — the field is still empty, but sworn to be empty rather than silently missing, and a person may approve of what was sworn to. What it does do: it blocks a Standing Order. StandingOrderGuardrails.Apply reads StandingOrderGuard.EmptyMandatoryFields before a host’s risk scorer is ever spent, and if any proposed field marked mandatory carries an Empty tag in force it degrades the verdict from StandingOrder to ReviewerConfirmation with blocked reason mandatory-field-empty — protocol rule GT-5, and the framework’s own act rather than a reviewer-UI convention. See Review Gate & Write Executors for that guard and the one beside it. What a reviewer surface makes of the pair is its own business: the quickstart sample’s <affiant-evidence-card> element, for instance, flags a field from its provenance alone — an Empty source, or zero confidence — and appends the required label separately. This mirrors Rule 2 of the seven normative rules: the same data serves the LLM’s reasoning and the UI’s rendering without either side inventing meaning the other didn’t provide.

Every field value the framework tracks carries a ProvenanceTag. The listing below is the type’s shape, not code to paste — every public member is named and every body is omitted (Beats is shown in full further down), and one attribute is dropped: the [JsonIgnore] the source carries on IsBound, which keeps a derived convenience off the wire.

public sealed record ProvenanceTag(
ProvenanceSource Source,
float Confidence,
[property: JsonPropertyName("note")] string? Evidence,
int? ConversationTurn,
ProvenanceBinding? Binding = null,
DateTimeOffset? At = null)
{
public float Confidence { get; init; } // overrides the positional property, to clamp it
public bool IsBound { get; }
public static bool RequiresBinding(ProvenanceSource source);
public bool Beats(ProvenanceTag incumbent);
public static ProvenanceTag Empty { get; }
public static ProvenanceTag FromTool(string toolName, float confidence = 0.9f);
public static ProvenanceTag FromInference(InferenceSource source, string fieldName, float confidence = 0.6f, ProvenanceBinding? binding = null, DateTimeOffset? at = null);
public static ProvenanceTag FromDefault(string reason, float confidence = 0.3f);
public static ProvenanceTag FromUser(string fieldName, ProvenanceBinding? binding);
}
  • Source — which of the seven ProvenanceSource values produced this value. Covered in full below.
  • Confidence — a 0.0–1.0 score. FromUser always assigns 1.0f; the other factories default to 0.9f (FromTool), 0.6f (FromInference), and 0.3f (FromDefault), each overridable via the optional parameter. ProvenanceTag itself clamps confidence into [0, 1], and an Empty tag always reads 0 — a producer reporting 1.4, -0.2, or NaN gets 1, 0, and 0 back, because the clamp lives on the record rather than at each mint site, so no caller can route around it.
  • Evidence — a human-readable explanation of why this source and confidence were assigned, e.g. "Extracted from SearchCustomers" from FromTool, or — from FromInference"Inferred from the turn: priority" and "Literally present in the turn: priority", the two sentences the protocol fixes so that two implementations swearing to the same facts hash to the same canonical bytes. This is what the Evidence Card carries so a reviewer can see why, not just what. (On the wire this property is spelled note — see Transport & Wire Contract — but the C# name is unchanged. The quickstart sample’s vendored <affiant-evidence-card> element still reads tag.evidence at 1.0.0-beta.3, so it renders no sentence for this property at all — and its guard tests only for null and "", which undefined passes, so the element appends an empty paragraph and the field’s grid gains a blank 8px row where the explanation should be.)
  • ConversationTurn — which conversation turn produced the value, or null for sources that aren’t conversational (an external API lookup has no turn to point to).
  • Binding (ProvenanceBinding?) — what an auditor looks at to check the value, as one of a fixed set of five kinds: UtteranceSpan, ReviewerAct, FormInput, ExternalRef, and ComputationRef, each carrying its own Ref shape (a form field’s name, an external system and record id, and so on). ProvenanceTag.IsBound says whether a tag points at anything at all; ProvenanceTag.RequiresBinding(source) says whether its grade ought to. Pass a real binding whenever there’s something concrete to point at — the artifact a claim rests on — and binding: null only where there is genuinely nothing to name.
  • At — when the tag was minted, null when the producer did not stamp one. TaskInferenceStep — the domain-agnostic merge step that folds an inference port’s structured output into the Context Fabric’s chain for each field, and which the TaskInferenceMergeFilter : ICompletionStageFilter runs — passes its own injected clock’s reading (_time.GetUtcNow()) as FromInference’s trailing at argument, and a reviewer’s accepted amendment stamps it from the decision’s own instant rather than a wall clock, so a caller can’t date its own agreement and a correction can’t be back-dated inside its own deadline.

The factories exist so plugin and inference code never hand-constructs tags with ad hoc confidence numbers. FromTool is for values lifted directly out of a deterministic tool result; FromInference is for values an LLM inferred, and takes an InferenceSource first — the enum has exactly two members, Inferred and Conversation (the value was literally present in the turn), so the inference path has no way to mint a UserStated, External, or Computed tag; FromDefault is for deterministic fallback rules; FromUser is for values the user stated directly, always at confidence 1.0, and now requires a binding argument — pass the artifact the claim rests on (a ProvenanceBinding.FormInput for a value typed into a control, say) or binding: null where there is genuinely nothing to point at; an unbound UserStated tag is still recorded exactly as claimed, just as the weakest form of the strongest grade. Reach for ProvenanceTag.Empty — not a hand-rolled tag — whenever there is genuinely nothing to swear to.

A single tag captures a field’s current provenance. ProvenanceChain captures its history — the answer to “how did this field arrive at its current value?”. The listing below is the type’s shape, not code to paste — every public member is named and every body is omitted; Merge is shown in full further down, beside ProvenanceTag.Beats, the comparison it delegates to:

public sealed record ProvenanceChain(
ProvenanceTag Current,
IReadOnlyList<ProvenanceTag> Prior)
{
public static ProvenanceChain From(ProvenanceTag tag);
public ProvenanceChain Append(ProvenanceTag newer);
public ProvenanceChain Merge(ProvenanceTag candidate);
public ProvenanceChain AppendChain(ProvenanceChain other);
}

Prior is ordered newest-first. Append unconditionally promotes a new tag to Current and pushes the old Current onto Prior — used when a later turn definitively supersedes an earlier value, such as a reviewer’s amendment.

Merge is the more interesting operation, because it encodes the determinism hierarchy directly — by delegating the actual comparison to ProvenanceTag.Beats, the framework’s one implementation of the rule, so ProvenanceChain.Merge, the schema-driven projection, and the task-inference merge step can never disagree about which tag wins. The fence below is two excerpts rather than one type’s code — Merge on ProvenanceChain, Beats on ProvenanceTag — and the comment inside Beats is shortened from the source’s, which also cites the protocol’s rules PV-2 and PV-3:

public ProvenanceChain Merge(ProvenanceTag candidate)
{
ArgumentNullException.ThrowIfNull(candidate); // as do Append and AppendChain
if (candidate.Beats(Current))
return Append(candidate);
var updatedPrior = new List<ProvenanceTag>(Prior.Count + 1) { candidate };
updatedPrior.AddRange(Prior);
return new ProvenanceChain(Current, updatedPrior);
}
public bool Beats(ProvenanceTag incumbent) // on ProvenanceTag itself
{
ArgumentNullException.ThrowIfNull(incumbent);
if (Confidence > incumbent.Confidence) return true;
if (Confidence < incumbent.Confidence) return false;
if ((int)Source < (int)incumbent.Source) return true;
if ((int)Source > (int)incumbent.Source) return false;
// Equal confidence, equal grade: the tag that points at something an auditor can
// go and check displaces the one that points at nothing.
return Binding is not null && incumbent.Binding is null;
}

Higher confidence wins outright. When two tags tie on confidence, the tag whose Source has the lower enum ordinal wins — which is exactly the determinism hierarchy below, because the enum is declared in that order. The hierarchy is a single ordered list rather than a separate ranking table for exactly this reason: the ranking is the enum’s declaration order, and Beats reads it directly off (int)Source. If both confidence and grade tie exactly, the tag pointing at something an auditor can re-check — a bound tag — displaces one pointing at nothing; an exact tie on all three (confidence, grade, and binding) leaves the incumbent in force, since the challenger brings nothing new. Whichever side loses, the losing tag is never discarded — it lands in Prior.

Merge is what happens, for example, when a task-inference step produces an LLM-inferred value for a field the Context Fabric already holds from a higher-confidence source: the higher-confidence value wins, and the loser is preserved — not discarded — in the chain.

ProvenanceSource is a closed, seven-value enum. The declaration order is the trust order, most deterministic first. The declaration below carries all seven values in their source order and drops two things: the per-value XML docs, and the type’s [JsonConverter(typeof(JsonStringEnumConverter))] attribute — which is what makes a grade cross as its own name rather than as an ordinal wherever it is serialised, including the persistence paths that build their own options rather than going through AffiantJson; see Transport & Wire Contract:

public enum ProvenanceSource
{
UserStated,
External,
Computed,
Conversation,
Inferred,
Default,
Empty
}
Source Meaning Default confidence from ProvenanceTag factories
UserStated The user explicitly stated this value in chat — e.g. “my email is [email protected]”. Maximal trust. 1.0
External Fetched from an authoritative external system: an API lookup, a database read, a third-party service response. — (no dedicated factory; construct directly)
Computed Derived by deterministic business logic — tax calculation, date math, priority-based SLA computation. Reproducible from inputs, not guessed. — (no dedicated factory; construct directly)
Conversation Mentioned in conversation context through a tool result, or literally present in the turn rather than stated as a value by the user. 0.9 via FromTool; 0.6 via FromInference(InferenceSource.Conversation, …)
Inferred LLM-inferred from conversational signals rather than read or stated directly. Nothing in the framework keys on the grade by name; what it puts in an approval policy’s hands is the low default confidence. 0.6 via FromInference
Default A system default or fallback value applied when no conversational basis exists. 0.3 via FromDefault
Empty Provenance is unknown. Not “low trust” — the explicit, sworn statement that the framework has no basis for a value at all. 0.0 via ProvenanceTag.Empty

One deliberate omission: there is no HumanCorrected source. When a reviewer sets a field during approval, the amendment is recorded as a new tag with source UserStated at confidence 1, carrying a ProvenanceBinding.ReviewerAct naming the decision and an At instant taken from the decision itself rather than a wall clock — a reviewer’s correction is, evidentially, the same kind of ground truth as a user’s direct statement, but the record still says whose act it was and when. What a reviewer who clears a field gets depends on which of the two folds ran, and the rule is not the same for a mandatory field and an optional one. On the accept path (AffidavitAmendments.Apply), a cleared mandatory field stays on the record and reads ProvenanceSource.Empty at confidence 0, under the same ReviewerAct binding and the same instant: a cleared field has no value and so cannot have confidence in one, and writing the reviewer’s maximal tag over an emptied field would make the confidence numbers rise as the record was wiped. A cleared optional field on that same path leaves the field list altogether and gets no tag at all — a reviewer clearing an optional field is saying the write no longer proposes it. On the resubmission path (AffidavitAmendments.Prefill, the same fold with keepCleared: true) neither of those rules applies: a cleared field — mandatory or optional — stays on the record carrying UserStated at 1 with the note “Cleared by …”, under the same ReviewerAct binding and the decision’s own instant, because there the clearing is the reviewer’s own act being shown back to them rather than a value nothing is known about. Wherever the field stays on the record, the tag it displaced is not discarded; it survives beneath the new one in the chain’s Prior list. The taxonomy stays clean this way: ProvenanceSource describes where a value came from, not what subsequently happened to it — that second question is what ProvenanceChain is for.

This is the seventh of the framework’s Seven Normative Rules: every Affidavit field carries provenance, no exceptions. If a field’s origin is genuinely unknown, it must be tagged ProvenanceSource.Empty — never left untagged, never omitted from the array.

The reasoning is adversarial, not stylistic. A field with no provenance tag and a field tagged Empty look identical to a naive reader — but no framework code path emits the first: AffidavitField.Provenance is a non-nullable ProvenanceChain and ProvenanceChain.Current a non-nullable ProvenanceTag, so a producer with nothing to swear to takes the path that produces ProvenanceTag.Empty instead. Non-nullable is a compile-time annotation rather than a run-time guarantee, and the framework does not rest on it: the compliance harness checks each field again at run time — field.Provenance is null || field.Provenance.Current is null — and records “field carries no provenance chain — Rule 7 requires every field carry provenance” against any producer that defeated the annotation. Omission is not a lesser evil than a wrong tag, because a missing tag is indistinguishable from “the framework forgot to track it,” while an Empty tag is a positive, checkable assertion that nothing is known.

The reviewer-facing consequence: the framework broadcasts the record rather than a rendering — an EvidenceCardRequest carries the whole Affidavit, every field’s provenance with it — and the host’s reviewer surface decides how to show it. The quickstart sample’s <affiant-evidence-card> element shows each field’s source as a badge beside a confidence meter, mutes the Inferred and Default badges, and gives a field whose tag reads Empty or whose confidence is 0 the warning colour and a sentence naming what is missing — “No source and no confidence — nothing stands behind this value.” for an Empty tag, and “Zero confidence in this value.” for a sourced field at zero confidence (the element carries a third sentence, “No source recorded for this value.”, for an unsourced field that still reports confidence, which no framework-minted tag can produce because an Empty tag always reads 0), so a reviewer approving an Affidavit is never guessing which fields are solid and which are the LLM’s best guess. A value present alongside ProvenanceSource.Empty is a hollow signature — a value asserted with nothing sworn about its origin — and AffidavitSubstance states that rule, with the two beside it (no fields at all; no field carrying anything but Empty), in one place read by two callers: ReviewGate refuses such a proposal at run time, throwing AffiantSubstanceException with nothing filed, nothing broadcast and no reviewer asked, and the schema-driven projection reports the same three conditions as the affidavit.refused.substance telemetry event. The compliance harness covers the same ground at test time without calling that predicate: AssertProvenanceIsSubstantive runs four checks of its own — no fields at all, a field carrying no provenance chain (a condition AffidavitSubstance has no equivalent of, because the chain is non-nullable), the hollow signature, and a Required schema field that projected without IsMandatory — and it deliberately lets an all-Empty Affidavit pass, because a case that legitimately infers nothing must be allowed to yield one. The all-Empty condition is enforced once per fixture instead: Verify records a failure against the strategy when no case in its fixture produced a substantive Affidavit at all. The hollow signature is the exact shape of a regression where a filter produced values without tracking where they came from, so it is refused rather than flagged.

Affidavit is what a WriteProposal carries out of a write tool — see Tool Envelopes. It is what the Docket holds while a mutation awaits review, and what the Evidence Card renders for the reviewer. Once approved, it is what a host’s IWriteExecutor receives — see Review Gate & Write Executors — as the one place an approved value becomes a real mutation in the host’s own system of record. That exclusivity is over the domain write: the framework’s own stores, the Docket among them, write their own rows without going through an executor.