Dead code is a tax on every future reader, every migration, and every agent context window — but deleting the wrong "dead" code is a production incident.
Tier: Core · Workflow area: Legacy Modernization
The discipline that makes sweeps safe: static analyzers and usage data nominate candidates, the AI assembles an evidence dossier per deletion, and nothing is removed without cited proof plus a grace period. The rule is absolute — no delete without evidence — because "the model was pretty sure" is not a rollback plan.
package.json/requirements.txt full of libraries nobody remembers adding — each one attack surface and upgrade drag.vulture (Python), knip (JS/TS — files, exports, and deps), depcheck, deptry, ts-prune, or compiler dead-code warnings.Generate candidates deterministically.
vulture src/ --min-confidence 80 > /tmp/dead-candidates.txt
knip --reporter json > /tmp/knip.json # unused files/exports/deps
deptry . > /tmp/deps-report.txt # unused + missing deps
Static tools over-report by design (dynamic dispatch, reflection, plugins, template references are invisible to them). Their output is a worklist, never a delete list.
Build an evidence dossier per candidate. This is where the AI earns its place — orchestrating the checks and writing up results, one candidate at a time:
Candidate: src/billing/legacy_tax.py::compute_gst_v1 (vulture, conf 90).
Build the evidence dossier by actually checking, citing each result:
1. STATIC: grep for the symbol across the repo — direct refs, string refs
("compute_gst_v1" as string: getattr, celery task names, URL confs,
templates, YAML/JSON configs, alembic/migrations).
2. RUNTIME: is it in the attached production coverage report? Any hits in
the attached endpoint access summary?
3. HISTORY: `git log --oneline -5 --follow` for the file — recently
touched? Referenced in any open PR/branch?
4. EXTERNAL: could anything outside this repo call it? (public API route,
exported package symbol, scheduled job defined in infra repo?)
VERDICT: DELETE (all checks negative, cite each) / KEEP (cite the hit) /
UNCERTAIN (name exactly which check couldn't be completed).
Anything the model cannot actually check must come back UNCERTAIN — an agent that can run grep and read the coverage file fills most dossiers itself; a chat model gets the outputs pasted in. UNCERTAIN is a fine verdict; a guessed DELETE is the only wrong output.
Handle dependencies with extra paranoia. For each unused-dependency candidate: grep for imports and side-effect usage (registered plugins, pytest plugins, polyfills, Django apps, webpack loaders — packages that do things when merely installed), check lockfile reverse-deps (npm ls <pkg>, pipdeptree -r -p <pkg>) so you don't remove something another dep needs peer-installed, and check config files that name packages as strings (.eslintrc, pytest.ini, babel.config). Then remove one per commit, full CI + build between each.
Delete with a tombstone process, not a hard drop. For DELETE verdicts:
logger.warning("DEPRECATED compute_gst_v1 called")), deploy, watch the logs for a cycle (a week of silence beats a month of speculation), then delete.Remove compute_gst_v1: no static refs (grep 2026-07-24), 0 prod calls in 30d coverage, last real change 2019 (a1b2c3d).Verify, ratchet, and re-run on cadence. After the sweep: full CI, build artifacts, smoke tests in staging, and — for endpoint deletions — a 404-rate check post-deploy. Then keep the floor from rising again: wire knip/vulture/deptry into CI as a ratchet (count may only decrease), and put the sweep on a quarterly cadence with the saved prompts. First-sweep etiquette: start with the lowest-risk tier (unused imports, unreferenced test fixtures, commented-out blocks) to calibrate your process before touching "unused" business logic.
Dynamic dispatch is where dead-code sweeps kill production — code invoked via getattr, string-named celery tasks, URL confs, template tags, and reflection shows zero static references while running daily. The string-reference grep in the dossier is not optional, and UNCERTAIN verdicts stay undeleted.
git bisect and git revert stay surgical.