writing-durable-docs
Use when writing or restructuring documentation — READMEs, guides, API references, onboarding material, architecture notes — when existing docs have gone stale or contradict the code, when a page is being corrected for a behaviour that changed, when deciding where a doc belongs or whether to delete one, when a doc is about to state that a platform or dependency cannot do something, when a single page is trying to be a tutorial and a reference at once, or when the same document exists in two places — a repo and a site, two repos, a wiki and a README — and nothing generates or diffs one against the other. Also use when writing a document a harness will carry across a compaction or handoff — a skill body, an agent instruction file, a plan a resumed session picks up — where anything past the opening may not survive the crossing. Covers doc types, rationale over mechanics, colocation, executable examples, stale-doc removal, and ordering rules so that what must hold longest comes first.
Overview
Documentation rots because it duplicates what the code already states, and the copy always loses. Durable docs record what code cannot express — the why, the constraint, the rejected alternative — and where they must state mechanics, they are generated or executed so drift breaks a build instead of misleading a reader.
When to use
- Writing a README, guide, reference page, or onboarding path
- A doc contradicts the code, or a documented step fails when run
- Deciding where a doc should live, or whether to delete it
- A page has grown four unrelated sections and nobody reads past the first
- Someone asks a question the docs "answer" but nobody could find
- Not for: why a decision was made and what lost →
writing-adrs. What changed between versions →writing-release-notes. Explaining one piece of work to one person →explaining-technical-work. Keeping a duplicated fact (a version, a constant, a mirrored non-prose artifact) from drifting once memory is no longer the plan →keeping-copies-in-sync. What belongs in an agent instruction file or a skill at all, as opposed to where in it a rule has to sit →writing-agent-instructions.
Four types, never two on one page
| Type | Reader's state | Serves | Fails when it |
|---|---|---|---|
| Tutorial | Has no problem yet; wants competence | A guaranteed-success path, one route only | Offers options or explains internals |
| How-to | Has a specific problem; is in a hurry | A recipe for one goal, assumes competence | Teaches concepts or starts from zero |
| Reference | Knows what they want; needs exact detail | Complete, structured, dry, no narrative | Tells a story or picks favorites |
| Explanation | Wants understanding; not at a keyboard | Context, why, trade-offs, history | Contains steps to follow |
The audience question that decides everything: does this reader already have the problem? No → tutorial or explanation. Yes → how-to or reference. Get this wrong and no amount of editing helps.
Merging is the default failure: a README that opens with a tutorial, drifts into a partial reference, and buries three how-tos. Every reader then reads the wrong 80% of it, and each type's edits corrupt the others — a tutorial gains options until it stops working, a reference gains narrative until it stops being complete.
Write the why; the what will drift
- The code is the source of truth for what happens. Prose restating it is a second source of truth that loses on the next commit.
- Three things worth writing down: the constraint (why the timeout is 30s and not 5s), the rejected alternative (why this is not a queue), the invariant not enforced by types (callers must hold the lock).
- Test: delete any sentence a reader could confirm by reading the code in under 60 seconds. If the whole doc dies this way, it should have.
- Same test at comment level.
// retry three timesis noise.// upstream rate-limiter returns 429 in bursts of ≤3; more retries deepen the burstsurvives every refactor of the retry loop.
Make drift fail a build
Ordered least to most durable. Move each doc up the ladder until the cost stops being worth it.
| Technique | Drift shows up as | When to stop here |
|---|---|---|
| Prose describing behavior | Silent rot | Only for explanation and why |
| Snippet pasted into prose | Silent rot | Never, past ~5 lines |
| Snippet included by reference from a compiled/tested file | Broken build or failing test | Good default for examples |
| Examples executed as tests (doctest-style, example functions, or a script that extracts fenced blocks and runs them) | Failing test | Best for how-to and tutorial |
| Reference generated from the source of truth — schema, types, IDL, CLI parser, migration files | Cannot drift | Always, for reference |
Rule of thumb: any code block over ~5 lines that nothing executes will be wrong within two releases. Either execute it or shorten it below the length at which it can be subtly wrong. Never hand-edit generated reference — the edit is lost and the reviewer learns to ignore that file.
The claims no build can check
Every rung of that ladder needs something to execute or generate. A claim that something is the case has a mechanism behind it; a claim that something is not the case has nothing to run, so it is pinned at the top rung — prose, silent rot — permanently.
Contains no telemetry. Sends nothing to a third party. Has no runtime dependencies. Never writes outside its own directory. Stores no personal data. These are the sentences readers rely on hardest, because they are usually promises about privacy, licensing, or safety rather than descriptions of a parameter list. They are also the ones nothing in the ladder above can protect.
They break differently, too, and that is what makes them slip. A positive claim goes stale when the thing it describes changes — the same diff is at least in the neighbourhood. A negative claim goes false when something is added somewhere else entirely, in a change that never touches the document and has no reason to. Nobody wiring up a new dependency thinks to grep the README for a sentence denying that dependencies exist. So a doc asserting what the software does is part of the software's surface: treat every such claim as a call site of the behaviour, and a behaviour change is not finished while a call site still says the opposite.
The defense is to invert the check rather than execute the doc. Assert the absence the sentence claims, as a test that fails when the forbidden thing appears: grep the built artifact for network calls, fail the build on a non-empty dependency list, assert the written-paths set. That drops an uncheckable prose promise to the bottom rung, where drift breaks a build. Where no such test is possible, the claim needs an owner and a review date like any unowned doc — and it needs to be written down somewhere the person adding the forbidden thing will actually pass, which is next to the code, not in the docs directory.
There is a second kind of negative claim, and it is worse, because it is about a system you do not own. The platform exposes no change-region metadata. The API has no way to page. The driver cannot report that field. A sentence like that is written to explain a design, and it reads as a description of the world; what it usually records is that the author did not find the capability, which is a fact about one afternoon's search. An inverted test cannot save it — there is nothing in your own artifact to assert the absence of — and it decays in the opposite direction from a promise: a promise about your software goes false when someone adds the thing, while a claim about someone else's is often false on the day it is written, and when it was true it goes false with no diff in your repository at all, because the dependency gained what the sentence denies. Nobody re-checks, because the sentence is the reason the work was never started. The cost is not a misled user but a door closed to every later reader who would have tried.
So a negative claim about a dependency carries its evidence or admits it has none: the specification section, the API version, the command that was run and what it printed — or the words not checked. That phrasing is honest, and it invites the next reader to check; not possible invites nobody. revalidating-decisions owns what a reader does with such a claim once it is recorded; the writer's half is to make it a claim with a date and a source, or to make it visibly a guess.
A changed behaviour is not documented until no mention of the old one is left
Correcting a document for a changed behaviour fails in one predictable way. You write or fix the section you were thinking about, and it is right; the same behaviour is still described elsewhere in that document, as a table row, an FAQ answer, a summary line, a store or marketing copy of it, each written at another time, in its own words, by someone not thinking about this change. Now the page contradicts itself, and a reader who lands on the row instead of the section believes the row. Nothing in the diff points at those places, because the diff is the part you edited, and a read-through tends not to find them either, since your attention is already on the new text. The call-site rule above applies inside a single document too: each of these is a place the document states the behaviour.
Find them by searching for the thing, not for the sentence: the name of the feature or entry point, its nouns and the other names it goes by, and the verbs of the old behaviour ("sends", "uploads", "directly", "automatically"). Do not search for the wording of your new text; the stale mentions, by construction, do not share it. Run the search over the document and over every copy of it, the mirror, the listing, the other-language versions. Read each hit, then edit it or keep it with a stated reason (still true for older versions, say). Put the search terms in the change description so a reviewer can repeat the search instead of trusting the claim that it was done. keeping-copies-in-sync covers the copies themselves and when the new text may be published.
Position is a reliability property
A document a person reads has one order, the one they read it in, and the only cost of putting a rule near the end is that fewer people get there. A document a harness reads on your behalf — a skill body, an agent instruction file, a system prompt fragment, a plan a resumed session re-reads — has a second reader, and that reader does not get to the end. Every mechanism that carries context across a boundary shortens the document, and the ones you did not choose shorten it from the end: a compaction keeps the opening of each file it re-attaches, a summarised handoff keeps the opening and paraphrases the rest, a capped listing keeps the entries that came first, a truncated tool result keeps its head. None of those mechanisms is told which sentence was load-bearing. They keep whatever came first.
So order stops being a readability choice and becomes a statement about which rules survive the run that needs them most. A constraint written at the bottom of an instruction file is in force for the first hour and silently absent after the first compaction, while the file's opening — usually its overview and the motivation for what follows — is the part that does. The failure is invisible from the inside, because the document still exists and still answers; it is just answering from a shorter version of itself than the one you wrote.
Write for that reader by putting first what must hold last: the hard constraints, the stop conditions, the things a later reader is not allowed to decide for themselves. Explanation, examples, and the reasoning behind a rule go after the rule, not before it — the reasoning is what a person needs to agree with the rule and the rule is what a machine needs to obey it, and only one of the two has to survive truncation. Then read the document's opening on its own — you cannot know where the cut will fall, so assume it keeps less than half — and ask whether an executor holding only that would still be bound by everything you meant to bind it with. If a rule you would not want dropped is below that line, moving it is the fix; padding the top with the reasoning that motivated it is not, because the padding is what pushes the rule out.
A mirror with no generation step is a fork
The same document in two locations is two documents. They drift from the system independently, and from each other, and nothing enforces the mirror because nothing knows it is one. Generate one copy from the other, or diff them in CI; those are the only two states that stay true. When you suspect drift and want it located cheaply, count occurrences of the load-bearing term in each copy — a large asymmetry points straight at the paragraph that moved in one and not the other.
Colocation and ownership
- A doc lives beside the thing it describes, in the same repo, ideally the same directory. Then a behavior change and its doc land in one diff and a reviewer sees the omission.
- Wikis, shared drives, and separate docs repos have no diff and no reviewer, so they rot invisibly and confidently. Move them in, or accept them with a named owner and a review date.
- Every doc has exactly one of: colocated with the code, generated from the code, or stamped with an owner and a review date. A doc with none of the three is unowned and will mislead someone.
- Link to stable identifiers — a permalink at a tag or commit, a named anchor, a symbol name — never a line number.
- Organize by reader task, not by system structure. A tree that mirrors the module graph is navigable only by people who already know the system.
Deleting is maintenance
- Wrong docs are worse than no docs. No docs sends the reader to the code; wrong docs are trusted, acted on, and cost more than the code would have.
- Delete on sight when: the feature is gone, the steps fail when run, the last edit predates the module's rewrite, or nobody will claim it.
- The content is in version control. "Might still be useful" is not a reason to keep a page that will mislead someone this quarter.
- 90%-correct pages are the most dangerous: readers who verify one claim extend that trust to the rest.
- Where policy forbids deletion, stamp a dated deprecation banner at the top with a pointer to the live source, above the content — not below it.
Common mistakes
| Symptom | Real cause |
|---|---|
| README is 800 lines, nobody reads past install | Four doc types merged into one page |
| Docs go stale after every release | They describe mechanics the code already states |
| New hire's setup guide fails at step 4 | The guide was only ever read, never executed |
| Doc says what a function does, reader still can't tell when to call it | Written from the author's mental model, not the reader's entry point |
| Everything is documented, nothing is findable | Organized by system structure; search returns the wrong altitude |
| Reference page disagrees with the schema | Hand-maintained copy of a source of truth |
| "We'll document it after the refactor" | Docs treated as a separate artifact instead of part of the change |
| Team writes docs, then answers the same questions in chat anyway | Doc answers the question the author had, not the one readers arrive with |
| A published doc denies a feature the software shipped weeks ago | A negative claim, which no example or generated reference can ever check |
| A page updated for a changed behaviour still states the old one in a table row or an FAQ answer | The section being edited was fixed; the other mentions were never searched for, by the feature's name and the old behaviour's verbs |
| Two copies of one document disagree | Mirrored by hand, so there is no generation step and no diff to fail |
| A capability the design doc says the platform lacks turns out to exist, and the implementation was never attempted | A negative claim about a dependency written from memory, with no source, read by every later reader as a reason not to try |
| An instruction file was obeyed for an hour and ignored for the rest of the run | The rule sat at the end of a document that gets truncated from the end each time context is carried forward |
Red flags
- "I'll write the docs at the end"
- Adding a fourth unrelated section to a README
- Copy-pasting a code snippet into prose
- Writing "simply", "just", or "obviously" — each marks a step the author no longer sees
- A page that explains the architecture to a reader who wanted one command
- Keeping a stale page "for reference"
- Documenting a workaround instead of deleting the reason for it
- A document promising the absence of something, with no test that fails when it appears
- A behaviour change documented in one section, with no search for the other places the document describes it
- "The platform does not support X" with no version, spec section, or command beside it
- The same guide living at two paths, neither generated from the other
- A hard constraint below the explanation that motivates it, in a file a harness will re-read after compaction