Doc-driven development
The shortest path to maintainable AI-augmented code: write the doc first, generate the code from the doc, keep them in sync.
The flow
The discipline: the doc is the contract. When code and doc disagree, you do not silently update the doc. You decide which is right and update both explicitly.
What "the doc" is
For a new ML feature, the doc has four sections at minimum:
# Feature: <name>
## What it does
One paragraph. Plain English. Audience is "next engineer in 6 months."
## Inputs and outputs
Schemas. Types. Example payloads. Edge cases.
## How to use it
A runnable code block. Copy-pasteable into a Python REPL or notebook.
## Performance characteristics
Throughput, latency, accuracy if relevant. Each number cites the script.
This fits on one screen. The agent generates code from it; reviewers compare implementation to spec.
Why it works with agents
The agent is good at:
- Generating code from a clear spec.
- Generating tests that match spec.
- Detecting drift when you ask "does the code still match the doc?"
The agent is bad at:
- Inferring intent when no spec exists.
- Maintaining unwritten conventions.
- Catching scope creep ("the function now does five things instead of one").
Doc-driven development plays to the strengths and avoids the weaknesses.
A worked example
Bad: "Write a function to dedupe the dataset."
Good:
# data.dedupe
## What it does
Removes near-duplicates from a corpus using MinHash + LSH.
## Input
- `df: pd.DataFrame` with at least column `text: str`
- `threshold: float = 0.85` — Jaccard similarity above which rows are duplicates
- `num_perm: int = 128` — MinHash permutations
## Output
- `pd.DataFrame` with same schema, near-duplicates removed
- Preserves row order: when collapsing a cluster, keeps the first occurrence
## Edge cases
- Empty DataFrame → returns empty
- DataFrame with one row → returns it unchanged
- Identical text → kept first occurrence
## Performance
Median throughput on a 100k-row corpus: <measured value goes here once we run benchmarks/dedupe.py>
Memory: O(n) MinHash signatures.
Now the agent writes the function, you write the test from the spec, the docstring on the function points back to this doc. Everyone knows what "works" means.
Keeping it in sync
The biggest failure mode: doc says X, code does Y, nobody notices for a quarter. Two practices that prevent this:
1. Doctest the code blocks
If the doc has a "How to use it" snippet, that snippet runs in CI. If it breaks, the build fails.
# tests/docs/test_doc_examples.py
import doctest, my_pkg.data
def test_data_doc_examples():
results = doctest.testmod(my_pkg.data, verbose=False)
assert results.failed == 0
2. The same-commit rule
Code and its doc move together. The next chapter goes deeper.
The trap to avoid
Don't write so much doc up front that the doc itself becomes a project. The target is one screen of spec for a one-day feature. Bigger features get broken into smaller docs.
The wrong instinct: "I'll write a 5,000-word design doc, then implement." That mode is for genuinely novel architecture. For day-to-day work, a one-page spec is the floor and the ceiling.
When this mode is wrong
- Pure exploration. You don't know what you're building yet. Write code, get signal, then write the doc.
- One-off scripts. A throwaway analysis script does not need a spec.
- Rapid prototyping during a hackathon-style sprint.
But once exploration concludes and the code starts being depended on, write the doc. Code that nobody can describe is code that can't be maintained.