DEV Community

Bala Paranj
Bala Paranj

Posted on

Your Docs are Outdated. You Just Don't Know Which Ones.

✓ Human-authored analysis; AI used for formatting and proofreading.

Every project has this problem. A developer changes how authentication works. The architecture doc still describes the old flow. A new CLI flag is added. The getting-started guide doesn't mention it. A function is renamed. Three tutorials reference the old name.

Nobody notices for weeks. A user files an issue: "the docs say X but the tool does Y." The maintainer fixes the doc. The same thing happens next month with a different doc.

The standard solutions don't work well. Code review discipline ("remember to update the docs") depends on human memory. CODEOWNERS files trigger reviewers but don't tell them WHAT to review. Doc-tests verify code examples compile but can't check whether prose descriptions are still accurate.

The root problem: the dependency between code and documentation is implicit. It lives in the maintainer's head. When the maintainer forgets or when a different developer makes the change, the dependency is invisible.

Make the dependency explicit

A documentation dependency manifest is a YAML file that maps code paths to the documents that describe them:

# docs-deps.yaml
dependencies:
  - sources: ["src/auth/**/*.go"]
    docs:
      - path: docs/explanation/architecture.md
        last_audited: "a1b2c3d"
    auto_regen: false

  - sources: ["src/cli/commands/**/*.go"]
    docs: ["docs/reference/cli-commands.md"]
    auto_regen: true
Enter fullscreen mode Exit fullscreen mode

Two types of dependencies, two different responses:

auto_regen: true means the doc is generated from code. Control catalogs, CLI references, API docs, schema references — anything where a template renders data the code produces. When the code changes, CI regenerates the doc automatically. No human needed.

auto_regen: false means the doc describes behavior that a code change may invalidate. Architecture explanations, getting-started guides, conceptual overviews — prose that a human wrote about how the system works. When the code changes, CI can't fix the doc. It can tell you the doc may be stale.

The last_audited field stores the commit hash of the source file at the time the doc was last verified. CI compares the current hash against the stored hash and reports drift: "this doc was last reviewed when auth.go was at a1b2c3d. The file has changed by 40% since then."

The CI workflow

On every push to main, three things happen:

Generated docs auto-fix. CI runs the regeneration step. If generated docs changed (new commands added, new catalog entries), it auto-commits. Counts, lists, and schema references never go stale because they're never hand-written.

- name: Auto-regen generated docs
  run: |
    make docs-regen
    if ! git diff --quiet docs/reference/; then
      git add docs/reference/
      git commit -m "auto-regen: docs from source data"
      git push
    fi
Enter fullscreen mode Exit fullscreen mode

Behavioral drift detection. CI checks each auto_regen: false entry. If the source changed since last_audited, it calculates drift and collects it.

Issue management. CI opens (or updates) a single GitHub issue listing every stale doc with its drift percentage. One rolling issue, not one per doc. When all docs are current, CI closes the issue.

- name: Open or update drift issue
  run: |
    STALE=$(cat /tmp/stale-count)
    if [ "$STALE" -gt 0 ]; then
      EXISTING=$(gh issue list --label "docs-drift" \
        --state open --json number --jq '.[0].number')
      BODY=$(cat /tmp/stale-docs.md)
      if [ -n "$EXISTING" ]; then
        gh issue edit "$EXISTING" --body "$BODY"
      else
        gh issue create \
          --title "Docs drift: $STALE documents may be stale" \
          --body "$BODY" --label "docs-drift"
      fi
    fi
Enter fullscreen mode Exit fullscreen mode

The weekly workflow: open the docs-drift issue, read the list, review each flagged doc, update last_audited hashes, push. The issue closes itself.

What belongs in the manifest

Six categories of code→doc dependencies:

Hardcoded counts. Any doc that says "3,000+ controls" or "supports 10 frameworks" depends on the data source. Map the data path to the doc. These should always be auto_regen: true — generate the number, don't type it.

Hardcoded lists. Any doc that enumerates supported features, output formats, or services. Same treatment as counts — generate from the source of truth.

Command output examples. Any doc that shows sample CLI output. When the output format changes, the example is wrong. Map the command source to the doc.

Behavior descriptions. Any doc that explains how a feature works. "The evaluator checks SCPs, then permission boundaries, then identity policies." If the evaluation order changes, this description is wrong. Map the evaluation code to the doc. auto_regen: false — CI warns, a human reviews.

Schema references. Any doc that describes field names, types, or enums. When a field is added or renamed, the schema doc is stale. These can be auto_regen: true if the schema is introspectable.

Configuration references. Any doc that describes config file format, environment variables, or flag names. Map the config parsing code to the doc.

How this compares to existing approaches

Most projects rely on one of five strategies. The manifest addresses what each one misses.

Code review discipline ("remember to update the docs") fails because it depends on the reviewer knowing which docs exist and which are affected by the change. The manifest makes the dependency explicit. The reviewer doesn't need to remember. CI tells them.

CODEOWNERS triggers a doc-team reviewer on doc file changes, but doesn't fire when code changes that SHOULD trigger a doc update. The dependency is backwards: CODEOWNERS watches the doc, not the code that the doc depends on.

Danger.js can implement the same logic as the manifest (check if source files changed, warn about affected docs). The difference is where the dependency lives: in a Dangerfile script (procedural, harder to audit) versus a YAML manifest (declarative, scannable). Either works. The manifest is easier to read and doesn't require a JavaScript dependency.

Doc-tests (Rust's approach) compile and run code examples embedded in documentation. This is the strongest solution for code examples. If the API changes, the doc-test fails. But doc-tests can't verify prose descriptions. "The evaluator checks SCPs first" isn't a testable statement. It's a behavioral claim that only a human can verify against the code.

Schema-driven generation (Kubernetes, Terraform) makes generated docs impossible to stale by generating everything from API schemas. This solves the auto_regen: true category completely. But not every doc is generatable. Architecture explanations, getting-started guides, and conceptual overviews require human prose. The manifest handles both: generated docs auto-fix, behavioral docs get drift detection.

The last_audited hash

The hash makes behavioral drift actionable instead of noisy. Without it, every push to a source file triggers a "docs may be stale" warning. After a week, you ignore the warnings.

With the hash, CI reports drift relative to the last time a human verified the doc. A 2% drift (one line changed in the source) is probably fine. A 40% drift (major refactor) definitely needs review. The percentage tells you which warnings matter.

When you review the doc and confirm it's still accurate (or update it), you set last_audited to the current commit hash. The drift counter resets. CI stops flagging that dependency until the source changes again.

# Before review: CI reports 40% drift
- path: docs/explanation/architecture.md
  last_audited: "a1b2c3d"   # 4 months ago

# After review: drift resets to 0%
- path: docs/explanation/architecture.md
  last_audited: "f7e8d9c"   # today's commit
Enter fullscreen mode Exit fullscreen mode

What this costs

The manifest is ~50 lines of YAML for a mid-size project. The CI workflow is ~40 lines. No new dependencies (Python parses the YAML, git computes the diffs, gh manages the issue). Total setup: about an hour.

The ongoing cost: when you add a new doc, add a dependency entry. When you add a new code path that docs describe, add a mapping. When you review a flagged doc, update the hash. Each takes 30 seconds.

What it saves: every "the docs say X but the tool does Y" issue you don't get. Every user who doesn't abandon your project because the getting-started guide describes a flag that was renamed three months ago. Every contributor who doesn't submit a PR based on an architecture doc that describes last year's design.

Documentation debt compounds silently. The manifest makes it compound loudly — in a single GitHub issue with a number next to each stale doc telling you how stale it is.


The pattern described here is built for Stave, an open-source cloud security tool with 3,000+ controls and generated reference docs. The manifest tracks dependencies between the evaluation pipeline code and the docs that explain how it works. Generated pages (control catalog, chain catalog, CLI reference) auto-regen on every push. Behavioral docs (architecture, exploitability classification) get drift-detected with last_audited hashes. The docs-drift issue is the single place I check weekly.

Top comments (0)