The Plan Is the Contract
Part 8 of 12 · Why AI Code Fails (And How to Fix It)
The Plan Is the Contract
Two developers. Same AI model. Same feature: guest checkout. One gives the AI the entire codebase and a task description. The other gives it six files and a precise written instruction.
They both get working implementations. The first PR has 847 lines across fourteen files. The second has 298 lines across six files. Everything in the second PR was expected. Eleven of the fourteen files in the first PR were not.
Here's what happened in the first PR.
The AI, with the full codebase as context, did what it naturally does: it noticed things. It noticed that the analytics service was missing a guest order event. It noticed that the order history screen had a performance issue that could be addressed while it was in the area. It noticed that the returns flow had a null safety warning it could suppress. So it fixed all of them, alongside the guest checkout implementation.
The analytics service change was reasonable. It was also untested, undocumented, and added a call to a function that doesn't exist in the production environment — only in the test harness the developer uses locally. The performance "improvement" to the order history screen changed a query in a way that broke the admin panel's weekly aggregation report. The null safety suppression hid a warning that was there intentionally as a signal for a known issue being tracked separately.
None of these problems were in the guest checkout feature. They were in the AI's helpful extensions of it.
The second developer's PR didn't have any of this, because the second developer gave the AI a bounded file list. Six files. Not the whole codebase. The AI couldn't touch the analytics service because it wasn't in the context window. It couldn't see the order history screen. The null safety warning in the returns flow was invisible to it.
That's bounded context. And it's the difference between a PR you can review in 30 minutes and one that requires a full day to understand what actually changed.
The implementation plan is the document that creates the boundary.
By the time a feature reaches Step 4, three things exist: a business intent, a set of requirements with acceptance criteria, and an ADR that records the key architectural decisions. The implementation plan is generated from those three documents — not from the task description, not from a verbal summary, not from the developer's memory of the planning conversation.
Here's what the guest checkout plan looks like at its core:
## Files to change
| File | Change | Task |
|---------------------------------------------------|-------------------------------------------------|---------|
| lib/models/order.dart | Add nullable userId, guestEmail; add assertion | TASK-01 |
| lib/services/order_service.dart | Add placeGuestOrder(); comment getOrdersByUser | TASK-01 |
| lib/screens/checkout/guest_checkout_screen.dart | New — equal weight CTA, checkout fields | TASK-02 |
| lib/screens/checkout/checkout_screen.dart | Add guest entry point | TASK-02 |
| db/rules/orders.rule | Guest read by orderId + guestEmail pair | TASK-03 |
| services/orders/index.ts | guestEmail index; GDPR deletion handler | TASK-03 |
Six files. That's the scope. If a file isn't on this list, the AI doesn't touch it. Not because it's prohibited by rule — because it's not in the context window when the AI runs.
The impact analysis in the plan is equally important. It explicitly addresses the files that aren't changing:
OrderService.getOrdersByUser()— must filteruserId != nullexplicitly; guest orders must not surface in a signed-in user's order history.Admin order panel query — no change needed; queries full collection regardless of userId (per ADR-012 rationale — this was a design goal).
Returns flow — already uses orderId + email lookup; guest orders work without any change to the returns handler.
This is the analysis that catches the dependency nobody thought about. Not when the code is reviewed. Not when CI runs. Before any code is written. The impact analysis asks: what else does this change? — and answers it in writing, in the plan, where fixing the answer costs an hour instead of a sprint.
The cost-of-discovery table from Step 4:
| Found at | Cost |
|---|---|
| Step 4 — impact analysis | An hour to update the plan |
| Step 6 — human verification | A day to rework the implementation |
| Step 9 — senior review | Full rework; PR closed and restarted |
| Production | Emergency fix, hotfix deployment, potential data integrity issue, client conversation |
The last row is not hypothetical. Most production incidents in AI-assisted codebases trace back to an assumption the AI made that no human ever validated — because there was no plan that required it to be written down.
Each task in the plan is a summary line. Before the AI executes it, the task is expanded into a precise written instruction — the file to change, the exact before and after state, constraints derived from the ADR, what the task depends on.
Here's the expansion for TASK-01:
Files: lib/models/order.dart and lib/services/order_service.dart
Add userId (nullable) and guestEmail (nullable) to the Order model.
Exactly one must be present — enforce with a constructor assertion
so the constraint is caught at object creation, not at runtime.
// Before
class Order {
final String id;
final String userId;
...
}
// After
class Order {
final String id;
final String? userId; // null for guest orders — per ADR-012
final String? guestEmail; // required when userId is null
...
Order({...}) : assert(
userId != null || guestEmail != null,
'Order must have either a userId or a guestEmail',
);
}
In order_service.dart, getOrdersByUser() requires no structural change.
Add a comment referencing ADR-012 so a future developer does not add
an explicit null check that breaks the guest order separation.
Depends on: nothing
The AI receives this instruction. Not a task description. Not "add guest checkout." A precise before/after specification of a single file, derived from the architectural decision already recorded in ADR-012, with the exact constraint from the ADR embedded in the comment it's asked to write.
The output is predictable. The review is tractable. The risk of drift is contained to this task, not to all fourteen files the AI might want to touch.
The approval gate in the plan is the mechanism that prevents Phase 1 mistakes from becoming Phase 2 problems.
⏸ APPROVAL GATE
Verify guest purchase end to end in the staging environment before
Phase 2 begins. Confirm: order created, confirmation email received,
guest order visible in admin panel, returns lookup resolves by
orderId + guestEmail. Do not proceed to Phase 2 until approved.
A gate in the plan is a hard stop. Generation does not continue past it until a human has confirmed the work completed in the current phase. If TASK-01 had a bug in the Order model constructor, the gate catches it before TASK-02 builds an entire guest checkout screen on top of a broken model.
This is slower than running all tasks in one session. A task-by-task approach takes longer per feature.
It also means problems are isolated. A bug in TASK-01 is a TASK-01 fix. A bug in TASK-01 discovered after TASK-05 was generated on top of it is a TASK-01 fix plus a TASK-05 rewrite plus a review of everything in between.
Small and isolated versus entangled and expensive. The plan forces the first. A chat-window approach produces the second.
The plan is a contract between the human and the AI. The human reviews it and approves it before generation starts. The AI executes within it and reports ambiguities it can't resolve rather than resolving them independently. The developer spots checks each task before the next begins. The senior reviewer at Step 9 confirms the implementation matches the plan.
This is the discipline that makes AI-assisted development reliable at scale — not a better model, not a more detailed prompt, but a document that commits the human decisions before the AI generates and holds both parties accountable to what was agreed.