Catch when your markdown docs drift from your Rust source code.
doc-drift verifies that external markdown documentation (the kind in docs/modules/, docs/architecture/, etc.) accurately reflects Rust source code. It loads your project using rust-analyzer's semantic analysis, extracts every public item, attribute, dependency, struct field, and enum variant, then compares all of that against your markdown docs.
No existing tool does this. #![deny(missing_docs)] and rustdoc only check inline /// comments. doc-drift checks your .md files.
External markdown docs are the first thing a new contributor reads and the first thing an AI assistant loads for context. When they drift from the code — missing items, stale references, undocumented dependencies — debugging and onboarding break silently. doc-drift makes that drift visible and CI-enforceable.
cargo install doc-driftCreate a doc-drift.toml in your project root:
[sources]
paths = ["src/"]
exclude = ["main.rs"]
[docs]
module_paths = ["docs/modules/"]
[mapping]
style = "flat"
separator = "-"Run it:
doc-driftOutput:
FAIL src/memory/store/episodic.rs
[missing-from-doc] public struct `EpisodicEntry` not found in doc
[missing-attribute] derive `Serialize` on `EpisodicEntry` not documented
[missing-dependency] dependency `crate::memory::store::types` not mentioned in doc
FAIL src/repair/orchestrator.rs
[no-doc] no matching doc found (expected: repair-orchestrator.md)
Summary: 2 failed, 4 diagnostics
Exit code 0 if all checks pass, 1 if any fail, 2 on errors.
| Check | What It Catches |
|---|---|
| Forward | Public item in code not mentioned in doc |
| Reverse | Identifier in doc that doesn't exist in code (stale reference) |
| Dependencies | use crate:: dependency not documented |
| Attributes | Significant derive/macro attribute not documented |
| Sections | Required heading missing from doc |
| Freshness | Source file newer than its doc |
| Content | Doc below minimum word count (stub detection) |
| Parameters | Function parameter name or type not mentioned in doc |
| Return Types | Custom return type not mentioned in doc |
| Variants | Enum variant not mentioned in doc |
| Fields | Struct field not mentioned in doc |
The first seven checks verify items exist in docs. The last four (depth checks) verify docs describe what items do — their parameters, return values, variants, and fields.
- Loads your Cargo project using
ra_ap_*crates (rust-analyzer's published internals) - Walks the module tree with full macro expansion (sees
#[derive(...)]output, proc macro attributes) - Extracts all items (pub and internal), struct fields, enum variants, module names
- Parses Cargo.toml for dependency crate names
- Parses markdown docs using pulldown-cmark (identifiers from inline code spans, headings, word count)
- Compares code facts against doc facts with intelligent filtering
The reverse check automatically knows about:
- All items in your project (any visibility)
- All struct fields and enum variants
- All module names
- All Cargo.toml dependency names (both hyphenated and underscored)
- All public items from every dependency crate
- 100+ Rust keywords, built-in types, and standard traits
No whitelist needed for standard Rust projects. Produces minimal false positives on typical Rust codebases with default configuration.
Full configuration reference
[sources]
# Directories to scan for .rs files
paths = ["src/"]
# Filenames to skip
exclude = ["main.rs", "connection_tests.rs"]
[docs]
# Directories containing module docs
module_paths = ["docs/modules/"]
# Directories containing cluster/group docs
cluster_paths = ["docs/clusters/"]
[checks]
# Headings that must appear in every doc
required_sections = ["What It Does", "How It Works", "Dependencies"]
# Minimum word count (catches empty stubs)
min_word_count = 50
# Check if source is newer than doc
freshness = true
# Attributes to not report (e.g., test-only attributes)
excluded_attributes = ["cfg(test)"]
# Additional identifiers to skip in reverse check
identifier_whitelist = ["OLLAMA_URL"]
[checks.depth]
# Verify function parameters are documented
parameters = true
# Verify custom return types are documented
return_types = true
# Verify enum variants are documented
variants = true
# Verify struct fields are documented
fields = true
[mapping]
# "flat" = src/memory/store/episodic.rs -> memory-store-episodic.md
# "nested" = src/memory/store/episodic.rs -> memory/store/episodic.md
style = "flat"
# Separator for flat style
separator = "-"
# Drop N leading path segments (e.g., 1 drops cluster prefix:
# src/scheduling/heartbeat.rs -> heartbeat.md instead of scheduling-heartbeat.md)
strip_prefix = 0doc-drift [OPTIONS] [PROJECT_ROOT]
Arguments:
[PROJECT_ROOT] Path to the Rust project (default: current directory)
Options:
-c, --config <FILE> Config file path (default: <PROJECT_ROOT>/doc-drift.toml)
-f, --format <FMT> Output format: human, json (default: human)
-h, --help Print help
-V, --version Print version
Use --format json for CI integration:
doc-drift --format json | jq '.summary'- Stable Rust toolchain (no nightly required)
- A Cargo project with a
Cargo.toml
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.