Skip to main content

docrot

CI PyPI Python versions License: MIT

Deterministic doc↔code drift checker for Python repositories.

docrot finds references in your prose documentation — READMEs, docs/, contributor guides, and agent context files like CLAUDE.md/AGENTS.md — that no longer resolve against your codebase: symbols that were removed or renamed, file paths that moved, make/tox/npm targets that are gone, and APIs that were documented but never shipped.

No LLM. No side-store. Every finding is backed by static analysis and git history, and comes with provenance: the commit where the claim was written, and the commit where it broke.

$ docrot
docs/advanced/transports.md:178:53  PY003 error  `httpx.Mounts` is documented but has never existed
    written at 3faa4a8 (2024-02-14)

docrot: 187 references | 82 resolved | 85 unknown (skipped) | 1 findings (1 error)

That output is real: on its first run against httpx (~14k stars), docrot found that the official transports documentation shows a complete usage example for httpx.Mounts — a class that has never existed in any commit of the codebase. On requests and flask, the same run produced zero findings — not because docrot checked less, but because their docs are actually in sync.

Why

A 2023 Empirical Software Engineering study (DOCER) measured that 28.9% of the top-1000 GitHub projects currently document a code element that no longer exists, and 82.3% have been in that state at some point. Since then the problem got teeth: AI coding agents read AGENTS.md/CLAUDE.md as ground truth on every session, so one stale path silently misdirects every run.

Adjacent tools deliberately stop short of this: doc example runners (sybil, xdoctest, pytest-examples) check fenced code blocks; docstring linters (docvet, pydoclint) check docstrings against signatures; link checkers (lychee) check URLs; AGENTS.md linters check file structure. None of them validate what your prose claims about your code.

How it stays quiet: the two-gate design

False positives are what killed every previous attempt at this, so docrot only reports when two independent gates agree:

Gate 1 — resolution (static analysis). References are resolved against a griffe-backed index of your package that understands star-import re-exports, alias chains (requests.Responserequests.models.Response), class inheritance (flask.Flask.route lives two base classes away), and instance attributes assigned in __init__. Every reference gets a tri-state verdict — RESOLVED, BROKEN, or UNKNOWN — and UNKNOWN never becomes a finding. External packages, dynamic attributes, tutorial placeholders: silence, not noise.

Gate 2 — history (git). A broken reference is only drift if it once resolved. docrot blames the doc line to the commit where the claim was made, re-resolves the reference at that commit (by parsing file blobs — no checkouts), and only then decides:

resolved when written? resolves now? verdict
yes no drift — reported, with the breaking commit bisected
no no fiction (a file the reader creates, pseudo-code) — silent
no, and it's your own package's namespace no documented API that never shipped — reported

The result on these three: requests 0 findings, flask 0 findings, httpx 1 finding, a real bug (709 references checked in total, a few seconds per repository). The full corpus run is in BENCHMARKS.md.

Install & run

pip install docrot
docrot                  # zero config: checks tracked *.md, *.rst + agent files
docrot --format json    # machine-readable, stable schema
docrot --show-unknown   # audit what was skipped and why
docrot explain PY002    # rule documentation

Requires Python 3.10+ and git. Without git history docrot still runs, but it says so and drops every finding to medium confidence, because the drift-versus-fiction evidence is exactly what history provides.

Command reference

Invocation Effect
docrot / docrot check check the whole repository (the default command)
docrot check docs/ README.md restrict the scan to those paths
docrot explain PY003 what a rule means and how to suppress it
docrot anchors update recompute anchor hashes after reviewing prose
Flag Effect
--format text|json|sarif|github output format (default text)
--rules core,agents,anchors enable only these rule packs
--fail-on error|warning|info lowest severity that fails the run
--strict medium-confidence findings also fail
--no-temporal skip the git gate (faster, lower confidence)
--show-unknown list every skipped reference and why
--changed-only only docs changed against HEAD (pre-commit mode)
--root PATH repository root (default: cwd)

Rules

Rule Fires when
PY001 Own-package symbol doesn't resolve (no git history to confirm)
PY002 Symbol resolved when the doc line was written — and no longer does
PY003 Own-package symbol is documented but never existed
PATH001 Documented path existed and is now gone
PATH002 File moved; docs still point at the old location
LINK001 Relative markdown link target missing
CALL001 Bare func() resolved when written, no longer resolves
AG001 Documented command target is gone (make/tox/nox/poe/npm/just/entry points/python -m)
AG002 Broken path in an agent context file (agents act on these)
AN001/AN002 Opt-in anchors: code changed under anchored prose

Suppress inline with <!-- docrot: ignore[PY002] -->; suppressions are counted in the report, never silent.

CI

GitHub Action (findings become PR annotations, optional SARIF upload):

- uses: actions/checkout@v4
  with:
    fetch-depth: 0        # full history = full provenance
- uses: dager23/docrot@v0

pre-commit:

repos:
  - repo: https://github.com/dager23/docrot
    rev: v0.1.0
    hooks:
      - id: docrot        # runs --changed-only for speed

Exit codes: 0 clean, 1 findings at/above fail_on severity, 2 usage error. Medium-confidence findings (no git history) fail only under --strict.

Configuration

Everything is optional. Use [tool.docrot] in pyproject.toml, or a standalone docrot.toml (same keys, no table header, takes precedence):

[tool.docrot]
packages = ["mypkg"]                 # default: auto-detected
exclude_docs = ["docs/archive/**"]   # changelogs are always excluded
severity = { PATH002 = "ignore" }
fail_on = "error"                    # error | warning | info
temporal = "auto"                    # auto | on | off
external_packages = ["werkzeug"]     # opt-in cross-package resolution
ignore_refs = ["flask.request.*"]     # glob-match references to skip
docs = ["**/*.md", "**/*.rst"]       # what counts as a doc
rules = { enable = ["core", "agents"], disable = ["LINK001"] }
strict = false

Unknown keys are reported and ignored rather than fatal. Changelogs, migration guides and upgrade notes are always skipped: naming removed APIs is their entire purpose.

Anchors

For prose whose meaning depends on specific code (not just references), bind a region to a symbol's implementation:

<!-- docrot:anchor symbol=mypkg.auth.DigestAuth hash=sha256:9f2ab8... -->
Digest auth performs two requests: the first receives the challenge...
<!-- /docrot:anchor -->

The hash covers the AST-normalized body, so formatting and comment churn never invalidate it — only structural change does. docrot anchors update recomputes hashes after you review the prose. Anchors are strictly opt-in: an absent anchor asserts nothing.

What docrot deliberately does not do

  • No LLM calls, ever. Semantic truth of prose isn't deterministically checkable; references and anchored claims are. docrot never guesses.
  • No docstring linting (see docvet, pydoclint), no example execution (see sybil, pytest-examples), no URL checking (see lychee). docrot is designed to sit alongside all of them.

Status

Alpha (0.x). Symbol resolution is Python-only; doc formats are Markdown, reStructuredText, and agent context files. Multi-language resolution (tree-sitter), CLI flag introspection, --fix rename suggestions, and an MCP server are planned.

Measured against 25 mature open-source projects: 6,073 references checked, 10 findings, every one confirmed by hand as a real documentation defect, and 139 of 139 seeded scenarios correct. The numbers and the method are in BENCHMARKS.md.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

docrot-0.1.0.tar.gz (68.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

docrot-0.1.0-py3-none-any.whl (49.9 kB view details)

Uploaded Python 3

File details

Details for the file docrot-0.1.0.tar.gz.

File metadata

  • Download URL: docrot-0.1.0.tar.gz
  • Upload date:
  • Size: 68.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for docrot-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6574d002b59973c0a609f1d371e23e878bd4c6a3eb88e46650355311639eb6a1
MD5 7e8cc4397164c581a02fdfe04ddf9a48
BLAKE2b-256 14e965549ae1cd1982a1fe1bc20ccae00bdf62488bf6df717f4d63880bff9574

See more details on using hashes here.

Provenance

The following attestation bundles were made for docrot-0.1.0.tar.gz:

Publisher: ci.yml on dager23/docrot

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file docrot-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: docrot-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 49.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for docrot-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bcaef698e28ce4ba0e993ceca98b8da11c02f258e498a8b2c260f4fddbb2f2eb
MD5 a2315189d446881078a5cb6f567c5493
BLAKE2b-256 e357e31dd78213c09e6ab76934a59375c27d525b6da18fd9b2667198ee8d7145

See more details on using hashes here.

Provenance

The following attestation bundles were made for docrot-0.1.0-py3-none-any.whl:

Publisher: ci.yml on dager23/docrot

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page