Dead code & mechanical fixes
Dead code is the cheapest debt to eliminate — if you can trust the analysis. ovecc
combines export usage (from real module resolution) with entry-point reachability,
reports what it finds with file:line, and can apply the mechanical part of the
cleanup itself, safely.
Detect
$ ovecc deadcode
Dead code: 8 unused export(s), 1 unused file(s), 1 unused dependency(ies), 1 unlisted dependency(ies)
[Medium] Unlisted dependency: dayjs
src/utils/helpers.ts:2 (bare import of 'dayjs' with no manifest declaration)
[Low] Unused dependency: left-pad
package.json:1 (dependencies)
[Low] Unused export: formatEuros in src/utils/helpers.ts
src/utils/helpers.ts:10
[Low] Unused file: src/legacy/old-report.ts
src/legacy/old-report.ts:1
[Low] Unused type export: Currency in src/utils/helpers.ts
src/utils/helpers.ts:4 (type-only)
...
Four distinct signals:
| Finding | Rule | Meaning |
|---|---|---|
| Unused export | unused-export | Exported, but imported by no reachable module |
| Unused type export | unused-type | Type-only export (interface/type alias) never imported — tagged separately because removal has zero runtime effect |
| Unused file | unused-file | Reachable from no entry point and imported by nothing |
| Unused dependency | unused-dependency (opt-in) | Declared in the manifest, never imported (detect_unused_deps = true under [index]; off by default because config-only usages cause false positives) |
| Phantom dependency | unlisted-dependency | Imported but missing from every package.json — works only via hoisting or a transitive install |
When nothing is flagged, the report states its coverage (entry points found, JS/TS files analyzed), so a clean bill is distinguishable from an analysis that never ran.
Fix — dry-run first, always
ovecc fix plans the mechanical remediation. It is
dry-run by default and prints exactly what would change:
$ ovecc fix
Fix plan: 10 change(s), 1 skipped — dry-run (pass --apply to write)
[planned] remove_unused_dependency — package.json:1 (unused-dependency)
remove "left-pad" from dependencies
[skipped] declare_dependency — package.json:2 (unlisted-dependency)
no lockfile version found for 'dayjs' — declare it manually
[planned] remove_unused_file — src/legacy/old-report.ts:1 (unused-file)
delete file (no entry point reaches it)
[planned] remove_unused_export — src/utils/helpers.ts:10 (unused-export)
- export function formatEuros(cents: number): string {
+ function formatEuros(cents: number): string {
[planned] remove_unused_export — src/core/db.ts:3 (unused-type)
- export interface Row {
+ interface Row {
...
What fix will do:
- delete unreachable files;
- drop the
exportkeyword on unused exports (and prune names from re-export lists) — the symbol stays if it's used internally; - remove unused manifest dependencies;
- declare phantom dependencies, pinned to the version the lockfile already resolves;
- delete stale
ovecc-ignorecomments.
What it will never do: anything requiring judgement. Default exports, architectural
smells, and security findings are never touched. Note the honest [skipped] above —
dayjs wasn't in the lockfile, so instead of guessing a version, fix tells you to
declare it manually.
Apply
$ ovecc fix --apply
Fix applied: 12 change(s), 1 skipped — applied
Safety properties of --apply:
- Every edit re-verifies the file against the index first and skips stale entries
with a reason — if the file changed since the last
index, ovecc won't blind-edit it. - Edits are format-preserving; JSON files (like
package.json) are re-validated after editing. - Scope it with
--rule:ovecc fix --apply --rule unused-export.
Verify the payoff
Re-index and check the trend:
$ ovecc index . && ovecc history unused_exports
History: unused_exports over 3 snapshot(s) 8 -> 0 (-8)
▇█▁
2026-07-11T09:35:24… d9416a8 8
2026-07-11T09:35:25… 1a67876 10 +2
2026-07-11T09:36:59… e8fa485 0 -10
Then keep it clean: ovecc deadcode --changed-since origin/main in CI reports only
dead code touching the current diff, and the
baseline ratchet
stops the backlog from growing.