Skip to content

Docket & Evidence Cards

A WriteProposal — the envelope a write tool returns instead of touching the database, covered in Tool Envelopes — does not sit in memory waiting for a reviewer. It is filed into the Docket, a durable, persisted queue of pending proposals, as a DocketEntry. What a reviewer actually sees on screen is an Evidence Card: the wire payload that renders the entry’s Affidavit — every field, its proposed value, and its provenance — as something a human can approve or reject. This page covers the DocketEntry lifecycle, the IDocketStore contract that persists it, the Evidence Card payloads that carry it to and from the reviewer, and Referrals — the delegated-review path where the reviewer isn’t the same person who triggered the proposal.

DocketEntry is defined in Affiant.Abstractions.Models:

public sealed record DocketEntry(
Guid EntryId,
string SessionId,
string TenantId,
string UserId,
string? ReviewerUserId,
string OperationType,
Affidavit Envelope,
ReviewStatus Status,
DateTimeOffset CreatedAt,
DateTimeOffset ExpiresAt,
IReadOnlyDictionary<string, object>? Amendments);
  • EntryId — a Guid that is both the entry’s identity and its idempotency key. The same EntryId filed twice is a no-op, not a duplicate entry (see IDocketStore below). It’s also the value that flows through the framework under two other names: EvidenceCardRequest.DocketId and ReviewOutcome.DocketId refer to the same Guid — the framework doesn’t invent a second identity for the same pending review.
  • SessionId, TenantId, UserId — the conversation, tenant, and user that produced the proposal.
  • ReviewerUserIdnull when the entry is self-reviewed by the same user who proposed it; set to a different user ID for a delegated review. See Referrals below.
  • OperationType — the tool name that produced the proposal, copied from WriteProposal.ToolName.
  • Envelope — the Affidavit itself: the sworn, per-field-provenance record covered in Affidavits & Provenance. This is what the Evidence Card renders.
  • Status — a ReviewStatus value; see the state machine below.
  • CreatedAt / ExpiresAt — when the entry was filed and when it stops being actionable. The gap between them is a fixed 10 minutes, set as a constant inside the ReviewGate service that files entries. In the current beta, that constant is not exposed as a per-host or per-policy configuration value. See Review Gate & Write Executors for where that constant lives and how filing works.
  • Amendments — fields a reviewer changed before approving, keyed by field name. This is supplied when the entry is filed (via the ReviewContext a host builds for the proposal) and, on approval, is what a host’s IWriteExecutor receives alongside the Affidavit so the executor can apply the reviewer’s edits rather than the LLM’s original proposal — see Review Gate & Write Executors for that hand-off.
public enum ReviewStatus
{
Pending,
Approved,
Rejected,
Amended,
Expired,
Cancelled,
Deferred
}

Every entry starts Pending. From there, the ReviewGate service (covered in full on Review Gate & Write Executors) is what actually drives entries to Approved, Rejected, Expired, or DeferredDeferred is the status a Referral produces, discussed below. Amended and Cancelled exist in the enum for a host to record additional review outcomes directly — for example, a host UI that lets the original proposer withdraw a still-pending request could mark it Cancelled by calling IDocketStore.UpdateReviewStatusAsync itself, without going through ReviewGate. ReviewGate recognizes Cancelled if it later re-reads the entry and reports it back as a rejected outcome, so a host is free to use that status without breaking the framework’s own bookkeeping.

The same file defines a ReviewStep record — ReviewerId, Status, ReviewedAt, an optional Comment — intended to capture one step of a multi-step review history. In the current beta, it is declared but not yet referenced by DocketEntry itself, which has no Steps collection; a DocketEntry records only its single Status, not a history of prior review steps.

IDocketStore, in Affiant.Abstractions.Interfaces, is what makes the Docket durable rather than in-memory ceremony:

public interface IDocketStore
{
Task SaveContextAsync(string sessionId, ConversationContext context, CancellationToken ct);
Task<ConversationContext?> LoadContextAsync(string sessionId, CancellationToken ct);
Task FileDocketEntryAsync(DocketEntry entry, CancellationToken ct);
Task<DocketEntry?> GetDocketEntryAsync(Guid entryId, CancellationToken ct);
Task<int> UpdateReviewStatusAsync(Guid entryId, ReviewStatus status, CancellationToken ct);
Task<IReadOnlyList<DocketEntry>> ListPendingBySessionAsync(string sessionId, CancellationToken ct);
Task<IReadOnlyList<DocketEntry>> ListExpiredAsync(DateTimeOffset expiresBeforeUtc, CancellationToken ct);
Task MarkExpiredAsync(IEnumerable<Guid> entryIds, CancellationToken ct);
}

SaveContextAsync / LoadContextAsync persist a session’s ConversationContext — the accumulated EntityRef state the Context Fabric tracks — so a session can be rehydrated after a restart. The rest of the interface is the Docket proper.

FileDocketEntryAsync must be idempotent on EntryId: filing the same entry twice is a no-op, not a second row. This is what makes it safe for the framework to retry filing without risking a duplicate review appearing to two different reviewers.

UpdateReviewStatusAsync returns an int — rows affected, not a bare success flag — and that return value is load-bearing. Implementations must guard the update with something equivalent to WHERE Status = 'Pending', so a transition only succeeds against an entry that is still pending. A second call against an already-resolved entry (already approved, rejected, or expired) affects zero rows rather than transitioning it again. This is the framework’s double-submit guard: if a reviewer double-clicks Approve, or two requests race after a restart, only one of them actually changes anything, and the other one sees 0 rows affected and can look up the entry’s real, already-settled status instead. ReviewGate relies on exactly this contract — see Review Gate & Write Executors for how it reacts to a 0 result.

ListExpiredAsync and MarkExpiredAsync back the expiry sweep. Affiant.Docket ships a DocketExpiryService — a BackgroundService — that ticks every 30 seconds, calls ListExpiredAsync for anything whose ExpiresAt has passed, and bulk-transitions those entries to Expired via MarkExpiredAsync. MarkExpiredAsync is idempotent for the same reason UpdateReviewStatusAsync is guarded: entries that already moved off Pending between the list and the mark are silently skipped.

As of 1.0.0-beta.1, the three IDocketStore implementations are split across two packages, not one — Affiant.Docket ships only the in-memory store; the two SQL-backed stores moved to Affiant.EntityFramework, next to the AffiantDbContext and entity configuration they’re built on. This is a deliberate split, not an oversight: the SQL stores take AffiantDbContext as a constructor dependency, and having Affiant.Docket reference Affiant.EntityFramework to get it would have been an adapter-to-adapter dependency the framework’s own layering rule forbids (see Packages). The practical effect: installing Affiant.Docket alone no longer drags in EF Core, SQLite, or Npgsql.

In-memory — registered by Affiant.Docket’s own AddAffiantDocket, which is required on every host regardless of persistence choice, because it’s also what registers DocketExpiryService (the expiry sweep) as a hosted service:

services.AddAffiantDocket(options => options.UseInMemory());

SQLite or PostgreSQL — registered by Affiant.EntityFramework’s AddAffiantEntityFramework, which also supplies the matching IChatSessionStore. AddAffiantDocket() is still called, with no store selection, purely for the expiry sweep:

services.AddAffiantEntityFramework(ef => ef.UseSqlite(connectionString)); // or ef.UsePostgres(...)
services.AddAffiantDocket(); // no store selection here — EF already registered IDocketStore

Affiant.EntityFramework’s EntityFrameworkOptions carries the store-selection guard: calling more than one of UsePostgres/UseSqlite/UseInMemory on it throws InvalidOperationException. Affiant.Docket’s DocketOptions has no equivalent to trigger — it exposes only UseInMemory(), since UsePostgres/UseSqlite moved to Affiant.EntityFramework in the split described above. Unlike the pre-beta behavior, AddAffiantDocket() no longer throws when no store is selected — that’s the normal shape for a SQL-backed host, where Affiant.EntityFramework supplies the store instead. If a host registers neither an Affiant.EntityFramework SQL store nor Affiant.Docket’s in-memory one, AddAffiantCore()’s startup validator catches the gap at boot, naming the missing registration, rather than the first write failing silently against no IDocketStore at all.

Evidence Cards: the human-facing rendering

Section titled “Evidence Cards: the human-facing rendering”

An Evidence Card is not a distinct C# type — it’s the name for what a reviewer sees when the framework renders an Affidavit for approval. The wire types that carry it are in Affiant.Abstractions.Transport:

public record EvidenceCardRequest(
Guid DocketId,
Affidavit Affidavit,
DateTimeOffset RequiredBy);
public enum ApprovalDecision
{
Approved,
Rejected
}
public record EvidenceCardResponse(
Guid DocketId,
ApprovalDecision Decision,
string? Reason = null);

EvidenceCardRequest is sent to the reviewer’s session — DocketId matches the filed DocketEntry.EntryId, Affidavit is the full sworn record with every field’s provenance, and RequiredBy is the entry’s ExpiresAt, so the UI can show a countdown rather than let a reviewer approve something already past its window. EvidenceCardResponse is what comes back: an ApprovalDecision of Approved or Rejected, plus an optional Reason a reviewer can attach — most useful on rejection, where it becomes the framework’s own ReviewOutcome.Rejected.Reason.

Both types travel over the transport layer’s own event vocabulary — TransportEvent.EvidenceCardRequest going out, TransportEvent.EvidenceCardResponse coming back — described in full in Transport & Wire Contract. What actually turns an EvidenceCardRequest into a visual card — colored provenance indicators per field, a countdown to RequiredBy, Approve/Reject controls — is host UI code; the framework’s job stops at handing over a fully-formed, fully-sworn Affidavit and waiting for a decision. See Review Gate & Write Executors for the service that sends the request and blocks on the response.

Not every entry is reviewed by the person who triggered it. DocketEntry.ReviewerUserId is how the Docket represents that: null means the same user reviews their own proposal; a non-null value names a different reviewer the entry is delegated to. When that delegation happens because an approval policy decided the operation needs escalation — rather than because a host always routes a given operation to a specific role — the framework calls it a Referral.

A Referral is implemented as an IApprovalPolicy — specifically, a subclass of ReferralRuleBase in Affiant.Policies.Referrals — that matches an Affidavit against some condition (say, an operation above a value threshold) and names the user ID to escalate to. When a Referral matches, ReviewGate transitions the entry to ReviewStatus.Deferred instead of sending an Evidence Card and waiting, and returns a ReviewOutcome.Referral outcome rather than blocking on a response. The full mechanics of how IApprovalPolicy implementations are evaluated, registered, and ordered — including ReferralRuleBase and its sibling StandingOrderBase for auto-approval — belong to Review Gate & Write Executors, which covers the policy pipeline in depth. From the Docket’s point of view, the important fact is simpler: a Referral is just a DocketEntry sitting at ReviewStatus.Deferred with a ReviewerUserId that isn’t the original proposer, waiting the same way any pending entry waits — subject to the same ExpiresAt and the same WHERE Status = 'Pending'-style guard against being resolved twice.

The Docket is what makes a WriteProposal durable rather than a fire-and-forget in-memory promise: a host process can restart between an Evidence Card being sent and a reviewer clicking Approve, and IDocketStore is what lets the review resume from where it left off. See Review Gate & Write Executors for the service that files entries, evaluates approval policy, sends and awaits Evidence Cards, and hands an approved Affidavit to the one place in the framework a database write is allowed to happen.