Picture testing the sign-up table for a ride-hailing app's riders (its passengers). You ask a model for "edge cases that should fail an email check". It returns three: an empty string, rider.at.example.com and user+tag@example.com. You add them to the test suite as rows that must be rejected. The test fails on the third, because user+tag@example.com is a perfectly valid address and your rule accepts it. Now someone has to decide whether the test or the rule is wrong, and the easy fix is to tighten the rule until the model's guess passes.
This happens because a language model produces plausible text, not verdicts. It has no access to the rule that will run, so its label of "valid" or "invalid" is a guess about your contract rather than a fact about it. Used as an oracle, that guess can quietly reshape your quality rules.
This article is about where AI genuinely helps with test data (realistic values, readable descriptions, scenario ideas), where it should not be trusted (deciding validity), and how LakeLogic keeps those two jobs apart. For the step-by-step mechanics of generating test data without any AI, see Generate failing data from your contract.
Where teams use AI for test data today
| Approach | How it works | Good fit | Watch out for |
|---|---|---|---|
| Ask a chat assistant for fixtures | Paste the schema, ask for rows or edge cases | Brainstorming cases you missed | Output varies; valid/invalid labels are guesses |
| Coding assistant writes a generator | The assistant writes Faker or Hypothesis code | A first generator, quickly | A second copy of your rules, which drifts |
| AI-based synthetic data products | Model-guided generation, often trained on a sample | Realistic, domain-flavoured values | Needs data or prompts; review what leaves |
| Learned synthesizers (SDV) | Statistical models trained on real data | Matching real distributions | Not an LLM; real data needs privacy review |
| No AI (Faker, rules, curated fixtures) | Deterministic generation and hand-picked cases | Repeatable CI | Generic values; only the cases you thought of |
Chat-generated values may also not match your field types, so check them before they reach a fixture file.
Each has its place. The gap is a decision most of them leave implicit: who decides whether a generated row is valid? If the answer is "the model", the test is only as good as the model's reading of your rules.
How LakeLogic divides the work
In LakeLogic Core the contract decides validity, always, by running. AI is optional and is used only where plausibility matters more than correctness. Here is what each feature actually does.
Realistic values (ai=True). DataGenerator.generate(ai=True) asks your configured model for realistic sample values per field (for example plausible campaign names instead of random words). The prompt asks for only one or two values per field, at a non-zero temperature, so variety is low and results change between calls. If the call fails, generation falls back to Faker and logs a warning; the log's Source: line says which was used. Model values are not checked against the field's rules when they are inserted into rows meant to be valid, so a model that ignores your accepted_values produces rows that the contract run will then quarantine. That is the contract doing its job, and a reason to always run generated data back through it.
Broken rows. When a contract declares rules on its fields (required, ranges, accepted values, patterns), LakeLogic builds each deliberately invalid row by breaking one declared rule, deterministically, and records which. It does not hand that decision to a model. Model-proposed edge cases are used only for fields where nothing is declared, and there they carry a different label, EDGE_CASE_AI, so you can tell an intended break from a model's guess.
Contract inference. infer_contract() derives types and suggested rules from the data itself, heuristically. With describe_with_ai=True it adds model-written column descriptions. The rules in the draft do not come from the model.
Reference project default. The RideFlow reference project's test-data notebook has a generative switch that is off by default and falls back to deterministic generation if no model is configured.
The opening example, run through a contract
Here are the model's three "invalid" emails, run through a contract whose only email rule is email LIKE '%@%.%':
import polars as pl
from lakelogic import DataProcessor
proposed = pl.DataFrame({
"rider_id": ["R1", "R2", "R3"],
"email": ["user+tag@example.com", "", "rider.at.example.com"],
})
good, bad = DataProcessor("contracts/rider_signups.yaml", engine="polars").run(proposed)
print("accepted:", good["email"].to_list())
print("quarantined:", bad["email"].to_list())
accepted: ['user+tag@example.com']
quarantined: ['', 'rider.at.example.com']
The contract (fields rider_id and email, both required, plus that one rule) accepted the address the model called invalid. Whether it should is your decision, made in the contract, reviewed like code. The model's suggestion was still useful: it asked a good question about plus-addressing. It just was not the answer.
A workflow that keeps AI in its lane
- Brainstorm with AI. Ask for scenarios ("riders with non-Latin names", "refunds on the same day"). Treat the answers as a list of questions.
- Encode decisions in the contract. Each scenario you care about becomes a rule or an accepted value, reviewed in a pull request.
- Generate deterministically for CI. Seeded generation, a pinned time window, no model call in the build.
- Use AI values for realism where it helps, such as demos or readable sample data, and expect them to vary.
- Let the contract run judge. Check intended validity against actual routing, and treat disagreements as findings.
When you might not need AI at all
- CI tests. Repeatability matters more than realism; deterministic generation is the better default.
- Sensitive schemas. If field names or descriptions should not leave your environment, use a local model or none.
- Simple types. IDs, amounts and timestamps gain little from a model.
Limits worth knowing
- Model output is not reproducible. A seed does not cover an external model call.
- AI values are few. One or two values per field gives realistic-looking rows, not coverage.
- A provider call is not a privacy guarantee. Field names, descriptions and rules are sent in the prompt. Check where your configured provider runs before turning it on.
- Model-validated extraction is a separate question. When an LLM extracts fields from documents, the same principle applies (the model proposes, the contract checks); Govern PDFs and PII with data contracts covers that case.
Key takeaways
- A language model can suggest values and scenarios; it cannot know whether your rule accepts them.
- Keep the validity decision in an executable contract, reviewed like code.
- Generate deliberately invalid rows from declared rules, not from a model's guess.
- Keep model calls out of CI; use them for realism and brainstorming.
- Always run generated data back through the contract and read the disagreements.
Next step
Start with deterministic generation and add AI only where it earns its place: Generate failing data from your contract walks through the full loop with runnable code.