Detecting Intent Drift Before It Ships: A Practical Framework
Code review catches bugs. It almost never catches intent drift, the quieter failure where the code is correct but no longer matches the requirement that justified writing it.
This distinction matters because intent drift is where AI-assisted development goes wrong at scale. When a developer asks an LLM to implement a feature, the model optimizes for the prompt in front of it, not the spec document approved three sprints ago. By the time the PR lands in review, reviewers are reading code, not auditing requirement fidelity. The drift has already happened.
Why Code Review Is the Wrong Defense
Code review is good at catching implementation errors: off-by-one mistakes, missing null checks, wrong argument order. It is genuinely bad at catching intent errors. Reviewers rarely hold the original requirement in their head when they read a diff. They read the code, understand what it does, and approve it if it seems reasonable. That is not a failure of process discipline. It is a cognitive limitation. You cannot simultaneously audit logic correctness and trace every code path back to a business rule stated in a Jira ticket from six weeks ago.
This gets worse with AI-generated code. LLMs produce syntactically clean, idiomatically reasonable code that reviewers tend to skim rather than scrutinize. The code looks like code a good engineer wrote. But the model's understanding of intent is bounded by the prompt it received, which was written by a developer who may have simplified, misremembered, or never actually read the original spec.
The result is a class of bugs that tests cannot catch, because the tests were written from the same prompt that produced the code. The spec said "notify the account owner on failed payment". The prompt said "send a notification on payment failure". The code sends the notification to the billing contact instead of the account owner. All tests pass. No reviewer flags it. It ships.
What Intent Drift Actually Looks Like
Intent drift is not always a semantic mismatch between two nouns. It shows up in several distinct failure modes that each require different detection strategies.
Scope creep from prompt rewriting. Developers summarize requirements when they write prompts. Summaries lose constraints. A requirement that says "apply discount only if the user has been active for 90 or more days AND has no outstanding balance" becomes a prompt that says "apply the loyalty discount". The generated code does what the prompt says. The original constraint is gone.
Silent scope reduction. The inverse problem: a prompt that is more conservative than the spec. The spec authorizes handling three payment states: pending, failed, and disputed. The developer's prompt mentions pending and failed. The disputed case gets no implementation and no error. It fails silently at runtime.
Behavioral drift across iterations. Over several sessions, an LLM is used to refactor a function. Each refactor is locally correct. Across the full sequence, a business rule gets simplified away because no single session held the full context of why it existed.
Constraint inversion. A requirement states what must NOT happen: "do not store PII in the audit log". A prompt for the audit logging feature focuses on what should be logged, never mentioning the exclusion. The generated code faithfully logs everything it sees.
That last one is the most dangerous. Negative constraints are nearly impossible to recover from a prompt alone, because absence of a thing is harder to specify than presence.
A Detection Framework That Actually Works
Detecting intent drift requires tooling that operates at the requirement level, not the code level. Here is a framework that scales from a single engineer to a team shipping with AI assistance daily.
Step 1: Anchor every change to a machine-readable intent
Before any code is written or generated, express the requirement as a structured artifact that tooling can parse. This is not a ticket. It is a contract. At minimum it should contain:
- The invariants that must hold after the change
- The behaviors explicitly excluded
- The acceptance conditions that constitute completion
A minimal example in ThunderLang's intent format:
intent:
id: billing-notify-owner-on-failure
requires:
- notification.recipient == account.owner_id
- notification.trigger == "payment_failed"
forbids:
- notification.recipient == billing_contact_id
accepts_when:
- integration_test: "notify_owner_on_failed_charge"
This is not documentation. It is an executable constraint that can be checked against a diff.
Step 2: Run a verify-diff before review, not during it
A verify-diff step compares the actual code change against the declared intent and surfaces violations before the PR opens. Think of it as a linter that operates on business semantics, not syntax. If the generated code routes the notification to billing_contact_id, the check fails with a traceable reason, not a vague test failure.
This shifts the detection point left. Reviewers stop being the last line of defense against intent errors. They become a second confirmation layer for an already-verified change. That is the correct role for human review: judgment, not audit.
Step 3: Require traceability artifacts on AI-generated changes
Requirements traceability in traditional software engineering means linking code back to requirements. With AI-generated code, that traceability needs to be automatic and durable, not a manual comment in a PR description.
Every AI-assisted change should produce a proof artifact: a machine-readable record that states which intent the change was evaluated against, which version of the spec it was checked against, and what the outcome was. Store these in version control alongside the code. When a bug surfaces three months later and the question is "did we ever verify this matched the spec", you have an answer that is not based on someone's memory.
Without this, spec drift accumulates invisibly. The codebase evolves, the requirements document evolves, and no one can say whether they ever matched each other at any specific point in time.
Step 4: Gate CI on intent, not just tests
Tests verify behavior. Intent checks verify that the behavior the tests cover is the behavior the spec required. Both belong in CI.
A CI pipeline that only runs tests can certify that the code does what the tests say. It cannot certify that the tests say what the spec intended. Add an intent gate that runs the verify-diff check on any PR touching code linked to a declared intent. Fail the build if the linkage is missing or if the check fails. This sounds strict. It is. That strictness is exactly what creates the forcing function that keeps AI-assisted changes auditable.
The Counter-Argument Worth Taking Seriously
The strongest objection to this framework is velocity: writing structured intent artifacts for every change adds overhead, and teams shipping fast will skip it. This is a real concern, not a dismissible one.
Scope the requirement intelligently. Not every change carries equal risk. A cosmetic copy change on a marketing page does not need an intent anchor. A change to payment routing logic, access control evaluation, or data retention behavior does. The discipline is in identifying which changes fall into the high-risk category and requiring the framework there. For everything else, the normal PR process is fine.
In practice, the overhead is smaller than critics claim. Writing a 10-line YAML intent block takes less time than the back-and-forth comment thread that happens when a reviewer suspects something is off but cannot articulate why. The structured artifact gives reviewers a reference point. It also gives the AI model a better prompt, because you are forced to specify the requirement precisely before you ask for code.
ThunderLang's getting-started guide shows how to set this up incrementally, starting with your riskiest change type and expanding from there. You do not have to boil the ocean on day one.
Gate your first AI change
If you are already using AI assistance to generate code, the intent verification layer is the missing piece. ThunderLang lets you declare what a change must satisfy, then gates AI-written code against that intent with a verify-diff and durable proof artifacts. Try it here.
The code that ships without an intent check does not just risk being wrong: it risks being wrong in ways your tests were specifically designed not to catch.