Skip to content

Documentation Generation That Doesn't Restate the Code

4 min read · updated August 3, 2026

// increments the counter by one above counter += 1 is the canonical output, and mocking it misses why it happens. Given only the code, a paraphrase is the most likely continuation. The fix is upstream of the prompt.

Why it paraphrases

Documentation is valuable exactly where it says something the code does not: why this exists, what it must never do, which invariant it upholds, what units the number is in, what happens on failure, what was tried first and rejected. None of that is recoverable from the source. A model given only the source and asked to document it has one available strategy, which is to restate.

So the question is not how to prompt around it. It is where the intent is written down, and whether you can put that in the context.

There is a second, quieter failure worth naming: documentation generated at the wrong altitude. Asked to document a module, a model will produce one paragraph per function, because functions are the structure it can see. What a reader needs from a module document is the opposite — what this module is responsible for, what it deliberately does not do, which of its exports are the entry points and which are incidental, and what you must not do to it. That shape has to be asked for explicitly, and it is the only shape at which a module document beats simply reading the code.

Where intent is actually recorded

More of it exists than people assume — it is simply not in the file.

  • The history of the exact lines. The most useful single command in this whole area:
    git log -L :priceLineItems:src/billing/invoice.ts --format='%h %ad %s%n%b'
    Every commit that touched that one function, with its message. Where a team writes real commit messages this is a complete design history, and it is the single best thing to paste alongside the code.
  • The pull request discussion. gh pr list --search "invoice locked" --state merged then gh pr view N --comments. Rejected alternatives live here and nowhere else, and “why not X” is the question documentation most often needs to answer.
  • Test names. A suite is a specification written in the imperative. test_locked_period_rejects_backdated_invoice states a rule the implementation only implies.
  • The deleted code. A guard that was added, removed and added again is a documented hazard; only the history shows the cycle.
  • The issue tracker. Frequently the only record of the customer situation that caused the constraint.

The prompt that surfaces the gap

Before asking for a document, ask for the questions the code cannot answer. This inverts the failure: instead of confidently filling gaps with paraphrase, the model enumerates them and you fill them.

Here is a module and the git log for its main functions.

Do not write documentation yet. List every question a new maintainer would
need answered that you cannot answer from what I have given you. For each,
say what evidence would answer it.

  1. What are the units of 'threshold'? (source: none — the type is number,
     callers pass both 30 and 30_000)
  2. Is issueInvoice safe to retry? It writes then publishes; is the publish
     idempotent? (source: the consumer, not shown)
  3. Why does resolveTaxRule special-case a missing vatId rather than
     rejecting? (source: the PR that added it, or a domain expert)

Answer those five sentences and the subsequent generated document is worth reading, because you supplied the only part that was ever missing. This costs about two minutes and is the difference between documentation and decoration.

What the difference looks like

Abstract advice about “capturing intent” is easy to agree with and hard to act on, so here is the same function documented both ways. The first is what you get from the code alone; the second is what you get after two minutes of answering the questions above.

/** Issues an invoice for the given organisation and period.
 *  @param orgId  the organisation id
 *  @param period the billing period
 *  @returns the created invoice
 */                                    // <- restates the signature. Delete it.

/** Creates the single invoice for an org's billing period.
 *
 *  Idempotent on (orgId, period): a unique index enforces one invoice per
 *  pair, and a second call returns the existing row rather than throwing.
 *  This is load-bearing — the billing job is retried by the queue and ran
 *  twice in the Feb 2026 incident (#4471).
 *
 *  Amounts are integer cents excluding tax; tax is resolved at issue time
 *  from the org's current country, NOT from the country at order time.
 *  That was a deliberate choice (#4102) and is wrong for backdated
 *  invoices, which is why issuing for a closed period throws.
 *
 *  Does NOT send anything. Delivery is the invoice-mailer's job, driven by
 *  the invoice.issued event.
 */

Every sentence in the second version is unrecoverable from the source: why idempotency matters, which incident proved it, the unit, the resolution rule and the case where it is knowingly wrong, and the boundary of the function’s responsibility. A model can write all of that fluently — it simply has to be told, and the git log and the issue numbers are where you get it.

Note also what the second version does not contain: no restatement of the parameters, no @returns that repeats the return type, and no description of the algorithm. Types carry the first two and the code carries the third. Documentation earns its maintenance cost only on the part nothing else can express.

Which documents are safe to generate

KindDescription
API referenceSafe and worth automating. Signatures, parameters, types and thrown errors are mechanically derivable, and the generator (typedoc, sphinx, godoc) does it deterministically. Use a model only for the one-line summaries — and only where the name is not already the summary.
Changelog entriesSafe from a diff plus the PR title, with a human deciding the category. The failure is cosmetic and visible.
Architecture decision recordsDraft the structure, never the decision. A model asked why a choice was made will produce plausible reasons, and a fabricated rationale in an ADR is worse than no ADR because it is cited later.
RunbooksDangerous. The steps must be true of your live system, and a confident wrong step is executed at 3am by someone who is not checking. Generate the skeleton, verify each command by running it.
Tutorials and getting-startedThe most dangerous of all: it will invent a flow that reads perfectly and does not work. Only ship one whose commands are executed in CI.
Inline commentsMostly not worth it. If the code needs a comment to be understood, the better edit is usually to the code. The exception is the non-obvious 'why' — and that is exactly the part a model cannot supply.

Making a stale document fail the build

Generated documentation goes stale exactly like a comment, and faster, because nobody feels ownership of prose they did not write. The only durable answer is to make examples executable, so that documentation lying about the code is a red build rather than a slow erosion of trust.

# Python: docstring examples are tests
pytest --doctest-modules src/billing/

# Rust: every fenced block in a doc comment compiles and runs
cargo test --doc

# Go: func ExampleIssueInvoice() with an // Output: comment
go test ./...

# Markdown: extract fenced blocks and run them
npx mdsh --frozen docs/getting-started.md

The rule that follows is simple and worth stating in your repo instruction file: an example in documentation is either executed by CI or is not an example. Everything else — the prose around it, the conceptual overview, the why — is written by a human, is short, and changes rarely, which is the correct division of labour anyway.

One more discipline worth adopting: date the document and name an owner in its front matter. Not for the reader — for the audit. A library of undated generated documents is indistinguishable from a library of stale ones, and the only cheap way to tell them apart is to have written the date down.

Documentation Generation That Doesn't Restate the Code · Multigrid