graveyard
graveyard finds dead code across Python, JavaScript, TypeScript, Go, and Rust in a single pass, then ranks every finding with git-aware confidence scoring. It exists because AI coding agents make cross-language dead code cheaper to create than to notice, and the existing toolchain still forces teams to stitch together language-specific scanners with no shared scoring model.
What is graveyard?
graveyard is a compiled Rust CLI that walks a repository once, extracts symbols with tree-sitter, builds a unified reference graph, folds in git age and churn, then emits a ranked report in table, JSON, CSV, or SARIF form. The front-door workflow is intentionally simple: use --min-age when you want "show me code that has been dead for a while" and use --min-confidence when you want CI-grade filtering across exported APIs, dead cycles, and fresh code that might still be in flight.
The repository scanner is manifest-aware, so pyproject.toml, package.json, go.mod, and Cargo.toml shape language detection automatically. .gitignore handling comes from the ignore crate, git history comes from git2, and the baseline commands let teams ratchet new dead code without having to clean an existing backlog in one change.
Installation
pip / pipx
npm
cargo
Homebrew
Quick Start
Run a scan in the current repository:
Filter for code that has been dead for at least a month:
Use CI gating with a stricter score threshold:
Sample terminal output:
CONFIDENCE TAG AGE LOCATION FQN
0.94 ExportedUnused 1.1 years src/lib.rs:42 src/lib.rs::legacy::old_api
0.88 Dead 8 months services/api/foo.py:17 services/api/foo.py::cleanup_task
Found 2 dead symbol(s) — min-confidence 0.8, min-age 30 days
Usage
The default scan targets the current directory and prints a ranked table:
Time-based filtering is the fastest way to adopt the tool in an existing repository:
Repository-specific controls map directly to the implemented flags:
Baseline management and language detection are first-class commands:
CI Integration
The ratchet workflow is the cleanest way to add graveyard to an existing codebase because it only fails the build when a pull request introduces new dead code relative to a stored baseline.
name: Dead Code
on:
pull_request:
push:
branches:
- main
jobs:
graveyard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install graveyard
run: pip install graveyard
- name: Enforce new dead code only
run: graveyard baseline diff --baseline .graveyard-baseline.json --ci
If you do not need a ratchet, replace the final command with graveyard scan --ci --min-confidence 0.8. SARIF output is available through graveyard scan --format sarif --output graveyard.sarif when you want to upload findings into GitHub code scanning.
Configuration
graveyard resolves settings in this order: CLI flags, .graveyard.toml, environment variables, then built-in defaults. The configuration file lives at .graveyard.toml by default and supports the full v1 surface.
[]
= 0.6
= "30d"
= false
= 0
= "table"
= "graveyard-report.json"
= ["migrations/**", "**/generated/**"]
= false
= ".graveyard-baseline.json"
= false
= false
[]
= 0.35
= 0.30
= 0.20
= 0.15
= 730
= 7
[]
= ["legacy_*", "TODO_*", "test_*"]
= ["migrations/**", "**/generated/**", "**/vendor/**"]
= ["@pytest.fixture", "@app.route"]
[]
= ["python", "javascript", "typescript", "go", "rust"]
[]
= ["main", "__main__", "app", "handler", "create_app"]
[]
= true
= "~/.cache/graveyard"
The GRAVEYARD_MIN_CONFIDENCE environment variable can override the default confidence threshold when the config file leaves it unset. NO_COLOR and GRAVEYARD_NO_COLOR both disable ANSI color output.
Understanding Scores
--min-age is the intended on-ramp because it maps directly to how engineers reason about stale code. If a symbol has had no meaningful touch for thirty days and still has zero reachable callers, it belongs high in the queue even before anyone thinks about the full formula.
--min-confidence exposes the full score for CI and team policy work. The score is a weighted sum of four factors: age of deadness, reference count, symbol scope, and recent churn. Local private functions with no callers and no recent history score higher than public APIs or code that changed this week.
confidence =
0.35 * age_factor(deadness_age_days)
0.30 * ref_factor(in_degree)
0.20 * scope_factor(symbol)
0.15 * churn_factor(commits_90d)
Those weights are configurable in [scoring], but they must still sum to 1.0. Use --ignore-exports when a repository has many intentionally public APIs and you only want truly unreachable internals.
Language Support
| Language | Status | Notes |
|---|---|---|
| Python | Yes | Functions, classes, __all__, decorator-aware extraction |
| JavaScript | Yes | Functions, arrow functions, exports, export * from |
| TypeScript | Yes | JS support plus interfaces, type aliases, TSX parsing |
| Go | Yes | Functions, methods, exported identifier detection |
| Rust | Yes | Functions, methods, structs, enums, pub visibility, test attributes |
vs. Other Tools
| Tool | Language Scope | Git History Scoring | Baseline Ratchet | Install Surface |
|---|---|---|---|---|
graveyard |
Python, JS, TS, Go, Rust | Yes | Yes | pip, npm, cargo, brew |
vulture |
Python only | No | No | Python |
knip |
JS/TS only | No | No | npm |
deadcode |
Go only | No | No | Go toolchain |
cargo-machete |
Rust dependency analysis | No | No | cargo |
graveyard is not trying to replace dependency-pruning tools such as cargo-machete. It sits at the source-code layer, where teams need one ranked list across a polyglot repository instead of separate outputs from five ecosystems.