
I want to start this issue from a place that feels very ordinary: a few insurance policy documents. Nothing exotic. No multi-agent architecture. No dramatic model behavior. Just policy records that a claims assistant might use when a customer asks about coverage, exclusions, or the next step in a claim.
That ordinary layer is exactly where many AI systems become fragile. We spend a lot of time discussing prompts, tools, model choice, vector indexes, and reasoning behavior. Then we quietly let stale documents, ownerless records, draft text, restricted data, or raw customer details become model context. Once that happens, the model is not the first problem anymore. The data boundary already failed.
In this issue, we build a local .NET data-contract pipeline for insurance policy documents. The contract decides which records are allowed to become RAG or agent context. It checks schema version, document type, owner, audience, sensitivity, freshness, effective window, canonical source URI, forbidden content patterns, and lineage. Only admitted records are exported into an index-ready JSONL file.
The Boundary I Care About Here
A claims assistant should not learn from every document that happens to exist in a folder. An expired policy, a draft rule, a restricted health claim export, or an unreviewed FAQ can all look useful at ingestion time while still being the wrong material for assistant knowledge.
Data teams already have language for this. The Open Data Contract Standard describes contracts with structure, quality, ownership, access, and service-level expectations. OpenLineage gives us a vocabulary for datasets, jobs, and runs. Great Expectations frames expectations as verifiable assertions about data. This issue does not reimplement those projects. It borrows the discipline and places it directly before AI context assembly.
That is the important move: before retrieval, before embeddings, before a model sees a paragraph, we ask whether the source record is allowed to become context at all.
What We Are Actually Building
The companion repository contains a small .NET console app named AiDataContracts. It has no model call. That is deliberate. I want the control layer to be visible without hiding it behind a successful answer from an LLM.
The app does this:
- loads a JSON data contract for insurance policy context
- loads a small insurance policy document dataset
- evaluates each document against deterministic admission rules
- rejects stale, unsafe, ownerless, restricted, draft, or expired records
- projects admitted records into an index-ready JSONL shape
- writes lineage for every admitted record
- writes a report that explains every admission and rejection
This is the kind of component I want in front of a vector index, a retrieval pipeline, an agent memory store, or a documentation context pack. It is boring in the best possible way. It gives the system a clear place to say no.
The Shape Of The Pipeline
The architecture is intentionally small. Source documents and the data contract enter the pipeline together. The validator makes the admission decision. Rejected documents stop there. Admitted documents are projected into retrieval-ready records and written with lineage.
Notice what is not in the diagram. There is no vector database yet. There is no embedding model yet. There is no agent loop yet. Those can come later. The first responsibility is deciding whether a record is eligible to become AI context.
The Contract Is The Product Interface
In this repo, the data contract is a JSON file. In a larger system it might be ODCS YAML, a catalog record, a governed dataset manifest, or a contract generated from a data platform. The format matters less than the engineering promise: the data producer and the AI consumer agree on what is allowed to cross the boundary.
The sample contract includes the fields that matter for AI context:
{
"contractId": "insurance-policy-context",
"contractVersion": "1.0.0",
"acceptedSchemaVersion": "1.0",
"minContentCharacters": 80,
"maxContentCharacters": 2400,
"allowedDocumentTypes": [
"coverage_summary",
"claims_process",
"exclusion",
"customer_faq"
],
"allowedSensitivityLevels": [
"public",
"internal",
"confidential"
],
"allowedOwnerTeams": [
"claims-knowledge",
"underwriting-standards",
"compliance-operations"
],
"maxReviewAgeDaysByDocumentType": {
"coverage_summary": 120,
"claims_process": 90,
"exclusion": 180,
"customer_faq": 60
}
}I like this contract because it is concrete. It does not say, "use good data." It says which schema version is acceptable, which document types are allowed, which owner teams can publish context, which sensitivity levels may enter the assistant, and how fresh each document type must be.
That is the difference between a policy and a slogan. A policy can fail a run.
The Insurance Example Makes The Risk Visible
The dataset has six records. Two are valid. Four are intentionally bad in different ways.
The valid examples are ordinary policy knowledge:
POL-AUTO-001: current auto collision coverage summaryPOL-HOME-002: current home water damage claim intake process
The rejected examples are more interesting:
POL-LIFE-LEGACY-003: an old FAQ that violates the freshness rulePOL-HEALTH-RAW-004: a restricted health claim export with raw member dataPOL-AUTO-DRAFT-005: a draft record with no owner, HTTP URI, future effective date, and "do not index" textPOL-TRAVEL-EXPIRED-006: an expired travel policy exclusion
Those are not artificial risks. They are the kinds of records that show up in real organizations. Archives are mixed with current material. Drafts live near approved documents. Sensitive exports get copied into convenient folders. Nobody owns the stale FAQ. If the ingestion job blindly embeds everything, the AI system inherits all of that mess.
Freshness Is Not A Nice-To-Have
Freshness is often discussed as if it were a retrieval ranking issue. It is deeper than that. In many AI systems, stale data is incorrect data. A model answering from an expired policy can be worse than a model saying it does not know.
The contract does not use one global freshness number. It uses a freshness window by document type. A claims process is held to a tighter review window than an exclusion document. A customer FAQ is tighter still.
The validator checks that rule explicitly:
if (contract.MaxReviewAgeDaysByDocumentType.TryGetValue(document.DocumentType, out var maxAgeDays))
{
var ageDays = (asOfUtc - document.LastReviewedAtUtc).TotalDays;
if (ageDays > maxAgeDays)
{
violations.Add(Blocking(
"freshness_sla_failed",
$"Document was reviewed {Math.Floor(ageDays)} days ago; maximum for '{document.DocumentType}' is {maxAgeDays} days."));
}
}
else
{
violations.Add(Blocking(
"missing_freshness_rule",
$"No freshness rule exists for document type '{document.DocumentType}'."));
}I prefer this shape because it is easy to challenge. If a business owner says a FAQ can be 180 days old, the contract change is visible. If the claims team says intake process documents must be reviewed every 30 days, that becomes a testable rule instead of tribal memory.
Access Belongs Before Indexing
A vector index is not a permissions system by itself. You can add access filters later, and many systems should. But the first question is more basic: should this record enter this AI product's context store at all?
The sample contract blocks restricted records. Each source record also declares allowed audiences. The run configuration says this pipeline is preparing context for claims-assistant. If the document does not allow that audience, it fails before indexing.
if (!contract.AllowedSensitivityLevels.Contains(document.Sensitivity, StringComparer.OrdinalIgnoreCase))
{
violations.Add(Blocking(
"sensitivity_not_allowed",
$"Sensitivity '{document.Sensitivity}' is not allowed for AI context."));
}
if (!document.AllowedAudiences.Contains(targetAudience, StringComparer.OrdinalIgnoreCase))
{
violations.Add(Blocking(
"audience_not_allowed",
$"Target audience '{targetAudience}' is not allowed to consume this document."));
}That matters because retrieval systems tend to make data more reusable. Reuse is useful only when the access model is already clear. If you put a restricted health claim export into a shared assistant index, you have created a new data product, whether you meant to or not.
Drafts And Raw Records Fail Closed
The contract also includes forbidden content patterns. In the sample project, those patterns catch raw personal identifiers, draft placeholders, and explicit "do not index" language.
"forbiddenContentPatterns": [
"\b\d{3}-\d{2}-\d{4}\b",
"\[\[TODO\]\]",
"do not index",
"raw member record"
]This is not a complete privacy system. It is a narrow gate. A production implementation would add better classifiers, field-level sensitivity rules, redaction policies, source-system permissions, and review workflows. Still, even this small gate catches a class of mistakes that should never reach a model.
I do not want the model to be responsible for ignoring raw member records. I want deterministic code to keep those records out of context.
Lineage Is How We Explain The Answer Later
When a document is admitted, the pipeline does not only keep the text. It also keeps lineage: source system, source version, canonical URI, source update time, contract id, contract version, admission time, and content hash.
The projected record has a shape like this:
{
"documentId": "POL-AUTO-001",
"title": "Auto Policy Collision Coverage Summary",
"metadata": {
"documentType": "coverage_summary",
"ownerTeam": "underwriting-standards",
"productLine": "auto",
"region": "US",
"sensitivity": "internal",
"sourceSystem": "policy-admin",
"sourceVersion": "2026.08.1",
"canonicalUri": "https://docs.example.insurance/policies/auto/collision-coverage",
"contentSha256": "..."
},
"lineage": {
"contractId": "insurance-policy-context",
"contractVersion": "1.0.0",
"sourceSystem": "policy-admin",
"sourceVersion": "2026.08.1",
"admittedAtUtc": "2026-08-22T00:00:00Z",
"contentSha256": "..."
}
}That lineage gives you something to inspect after a bad answer. Which policy version was indexed? Which contract admitted it? When was it admitted? Was the source document updated later? Did the content hash change? Those questions are engineering questions, not prompt questions.
A Local Run Tells The Story
Run the app from the companion repo:
dotnet run --project AiDataContractsThe output is deliberately plain:
AI Data Contracts
Contract: insurance-policy-context 1.0.0
Audience: claims-assistant
As of: 2026-08-22T00:00:00.0000000+00:00
Documents evaluated: 6
Admitted: 2
Rejected: 4
POL-AUTO-001 | Admitted
POL-HOME-002 | Admitted
POL-LIFE-LEGACY-003 | Rejected
- Blocking: freshness_sla_failed
POL-HEALTH-RAW-004 | Rejected
- Blocking: sensitivity_not_allowed
- Blocking: audience_not_allowed
- Blocking: forbidden_content_pattern
POL-AUTO-DRAFT-005 | Rejected
- Blocking: missing_owner
- Blocking: canonical_uri_not_https
- Blocking: document_not_yet_effective
- Blocking: forbidden_content_pattern
POL-TRAVEL-EXPIRED-006 | Rejected
- Blocking: document_expired
- Blocking: freshness_sla_failedThat is the behavior I want. The app does not quietly clean things up. It does not embed what it can and hope retrieval ranking saves the day. It makes the admission decision visible.
The Tests Protect The Boring Part
The tests focus on the contract boundary. They do not test model output because there is no model in this repo. That keeps the suite honest. The question is whether the gate admits and rejects the right records.
The suite covers:
- admission of a current owned document
- rejection of stale records
- rejection of missing owner metadata
- rejection of audience-disallowed records
- rejection of raw PII patterns
- rejection of expired policy records
- end-to-end dataset evaluation and JSONL export
dotnet test AiDataContracts.slnxThe current run passes 7 tests. The most useful test is the end-to-end pipeline check: six source records go in, two admitted records come out, and the JSONL export contains only those two admitted records.
Where The Model Finally Enters
After this gate passes, you can embed the JSONL output, load it into Azure AI Search, pgvector, Qdrant, Elasticsearch, a lakehouse vector index, or any other retrieval layer. You can use it in an agent. You can attach it to a claims assistant. The important part is that the context store is downstream of the contract.
This also changes how you debug model failures. If the assistant gives a bad answer, you can ask a better set of questions. Did the answer use an admitted document? Was the document fresh? Was it inside its effective window? Did the source version change? Did the contract version change? Was the wrong audience used? Was the lineage missing?
Those questions do not replace prompt evaluation, retrieval evaluation, or answer grading. They give those evaluations a stronger foundation.
The Deterministic Boundary
The probabilistic layer may summarize policy documents, answer customer questions, compare a claim against coverage language, or draft a response for a human handler. The deterministic layer owns the data admission boundary.
In this implementation, deterministic code owns:
- schema-version admission
- document-type admission
- owner-team admission
- audience admission
- sensitivity admission
- freshness checks
- effective-window checks
- canonical URI checks
- forbidden-content checks
- content hashing
- lineage emission
- index export
The model does not decide whether a draft record is safe. The model does not decide whether a restricted health export belongs in context. The model does not decide whether an expired exclusion should be treated as current policy. Those are system decisions.
Why This Architecture Works
The value is not in the size of the system. It is in where the decision happens.
- Bad records are stopped before they reach retrieval.
- Freshness, ownership, audience, and sensitivity are checked as admission rules.
- Drafts, raw records, and expired policies fail closed instead of becoming embeddings.
- Every admitted document keeps enough lineage to explain where it came from later.
- The JSONL export is simple to inspect and simple to replace with a real index writer.
- The tests protect the quiet boundary that the rest of the AI system depends on.
This is a focused control layer, not a full data platform. You can understand it, test it, replace parts of it, and put it in front of a real retrieval system.
Potential Enhancements
The next version of this project could emit OpenLineage-compatible events beside the local report. That would let the AI context-preparation job participate in the same lineage graph as the rest of the data platform.
It could also replace the local JSON contract with an ODCS-style contract, add data quality dimensions such as completeness and uniqueness, integrate a real vector index writer, and split freshness rules by product line, jurisdiction, and document risk class.
For regulated domains, I would also add field-level policy. A record might be valid as a source document but still contain fields that cannot enter a general assistant index. In that version, the contract would admit, redact, transform, or reject fields individually instead of treating the document as one block.
Those additions are useful, but I would not start there. I would start with the plain rule this issue demonstrates: no source record becomes AI context until deterministic code can explain why it was admitted.
Final Notes
A model can make weak context sound confident. It cannot turn stale policy, missing ownership, broken permissions, expired documents, or raw sensitive records into safe product knowledge.
The practical lesson is simple: do not let context enter the system just because it is available. Give it a contract, an owner, freshness rules, and lineage. Then let retrieval and generation build on top of material the system has already agreed to trust.
Explore the companion repository at the GitHub repository.
See you in the next issue.
Stay curious.
Join the Newsletter
Subscribe for AI engineering insights, system design strategies, and workflow tips.