Skip to content
0degrees.ai
Tooling

Documenting Code with AI: Keeping Docs Honest

How to get AI coding assistants to write documentation that accurately reflects your code — not plausible-sounding summaries that drift from reality.

0degrees Team 7 min read

AI assistants can write documentation in seconds. That speed is the trap. Documentation generated from a vague prompt tends to be fluent, confidently structured, and wrong in exactly the ways that are hardest to spot: it describes what a function should do based on its name, not what it actually does based on its implementation. A stale comment is annoying. A wrong docstring is a lie with syntax highlighting.

The goal of this post is to show how to get AI documentation that’s actually accurate — and how to recognize the contexts where you should skip the AI entirely.

Why AI documentation goes wrong by default

When you ask an AI to “document this function,” it reads the function name, its parameters, and its body, then generates prose that is consistent with all of those. Consistent with is not the same as derived from. The model has never run the code. It doesn’t know what happens when the input is malformed, what side effects are triggered, or which error conditions are real versus defensive dead code.

This produces a specific failure mode: docs that look right at the point of writing but drift as the code changes. A function gets a new early-return condition; no one updates the docstring because the docstring already looked fine. A year later the docs describe a behavior that hasn’t existed for months.

The fix starts with how you frame the request.

Generate docs from the implementation, not the other way around

The single most effective habit: write the code first, then ask for documentation in the same prompt that includes the finished implementation. Never ask AI to document a function it hasn’t seen.

# Wrong — model is guessing from intention
Write a JSDoc comment for a function that validates email addresses.

# Right — model is reading the implementation
Here's the finished validateEmail function:

  [paste code]

Write a JSDoc comment that accurately documents what this implementation
actually does: which inputs are accepted, what it returns, which errors
it throws, and any edge cases visible in the code. Do not add behaviors
that aren't in the implementation.

The phrase “do not add behaviors that aren’t in the implementation” matters. Without it, models happily add @throws {RangeError} for errors that the function never raises, or describe a return value that covers cases the function doesn’t handle. The explicit constraint stops the generative over-reach.

Getting JSDoc and docstrings right

For function-level documentation, specify exactly what you want the model to derive. A prompt that lists the fields you need is more reliable than one that asks for a generic docstring:

/**
 * Normalises a user-supplied tag string to the canonical stored format.
 *
 * Trims surrounding whitespace, lowercases the result, and replaces
 * internal whitespace runs with a single hyphen. Returns null if the
 * input is empty after trimming.
 *
 * @param raw - The raw tag string from user input.
 * @returns The normalised tag, or null if the input is blank.
 *
 * @example
 * normaliseTag("  Hello World  ") // "hello-world"
 * normaliseTag("   ")             // null
 */
export function normaliseTag(raw: string): string | null {
  const trimmed = raw.trim();
  if (!trimmed) return null;
  return trimmed.toLowerCase().replace(/\s+/g, "-");
}

To produce documentation at this level of precision, give the model a template to fill in rather than an open-ended request:

Write a JSDoc comment for the function below. Include:
- One-sentence summary of what it does
- Description of any non-obvious behavior (edge cases in the implementation)
- @param for each argument with type and description
- @returns with type and what null/undefined means if applicable
- @throws for any explicit throws in the body
- @example with 2-3 concrete inputs and outputs

Do not document behavior that isn't in the code.

[paste function]

The @example block is worth emphasizing separately. Models generate good examples when you ask explicitly, and concrete input/output pairs are usually the most useful part of a docstring for a future reader.

READMEs that don’t drift

README documentation has a different failure mode: it’s usually written once and then left to rot. AI can help with the initial draft, but the framing has to force the model to work from the actual repository state, not its prior assumptions.

Before asking for a README section, feed the model the relevant files:

Read the following files and then write the "Installation" and "Usage"
sections of a README for this project. Use only what's actually in
these files — do not assume steps that aren't here.

[package.json contents]
[src/cli.ts or main entry point]
[any existing .env.example]

Generating README sections one at a time — installation, usage, configuration, API reference — is more reliable than asking for the whole document in one shot. A single large prompt is more likely to produce coherent-looking prose that drifts away from reality in the later sections.

For projects with a configuration layer, have the model generate the configuration reference directly from the schema or config file. If you have a Zod schema or a TypeScript config type, paste it in and ask for a configuration table:

Here is the Zod schema for the project's config object:

[paste schema]

Write a markdown table with columns: key, type, required, default, description.
Derive the types, required status, and defaults directly from the schema.
Do not add configuration keys that aren't in the schema.

This produces documentation that’s structurally tied to the actual types, which means it’s wrong in an obviously detectable way if it drifts — the key names won’t match.

Keeping docs in sync during refactors

When you change a function with AI assistance, add an explicit documentation update step to your request:

Refactor the parseUserInput function to handle the new optional `strict`
flag. After refactoring, update the JSDoc to reflect all changes:
new parameters, any changed return behavior, and updated @example calls.

Without that second sentence, the model generates the new implementation and leaves the old docstring untouched. It’s not trying to mislead you — it just doesn’t treat documentation as part of the code change unless you say so.

For periodic audits of documentation drift, you can run a targeted sweep:

Here are the current JSDoc comments for the auth module and the current
implementation of the same functions. For each function, identify any
discrepancies between what the docstring says and what the implementation
actually does. List them as: function name, what the docs claim, what the
code does instead.

[paste docs and implementations side by side]

This works well as a step after a refactoring session where you’ve changed code without updating docs. It’s faster than reading every docstring against every function manually, and models are good at the pattern-matching it requires.

What AI documentation shouldn’t replace

There’s a class of documentation that AI generates poorly: the why, not the what.

Comments explaining that a function exists to work around a specific third-party library bug, that a field is stored denormalized for a reason that came out of a performance incident, that a particular error is intentionally swallowed because of a known upstream flakiness — these belong in the codebase, and they are almost never in AI-generated docs.

AI can tell you what code does. It can’t tell you why a past decision was made, what was ruled out before settling on this approach, or what invariant the code assumes that isn’t visible in the types. That context lives in PR descriptions, ADRs, and the occasional targeted inline comment written by the human who made the decision.

Something worth internalizing: AI documentation is most accurate when it’s derived from visible implementation, most helpful when it covers concrete input/output behavior, and least trustworthy for capturing the decisions behind the code. Keep the AI on the first two. Write the third one yourself.

The distinction lines up with what Evaluating AI-Generated Code Before It Ships calls plausibility bias — AI output tends to look complete even when important pieces are missing. A docstring missing a @throws for an error the function does raise will pass a quick read. Only a close check against the implementation reveals the gap.

The session structure that makes this work

Documentation tends to get treated as a cleanup step after the real work is done. The developers who keep it accurate use AI to fold it into the work itself: document each function at the point where it’s finished, before the context of what it was supposed to do has faded.

The practical structure:

  1. Write the implementation with AI assistance.
  2. In the same session, before moving on, paste the finished function back and request documentation using the structured prompt above.
  3. If refactoring existing code, include the documentation update as an explicit step in the refactoring request.
  4. After a major module change, run the documentation audit prompt to check for drift.

This doesn’t add much time per function — and it accumulates into a codebase where the docs are actually usable, which is a rarer outcome than it should be. For the context hygiene that makes these sessions stay coherent across a full feature branch, see Managing Context in Long AI Coding Sessions.

Keep reading

Tooling 7 min read

Using AI to Review Your Own Code Before It Ships

How to use AI coding assistants as a first-pass code reviewer — prompting for real feedback on code you wrote, interpreting what it finds, and fitting it into your workflow.

0degrees Team