---
name: codebase-audit
description: >
  Systematic codebase health audit — structural quality, dead code, type hygiene,
  test coverage, file sizing, naming conventions, and package boundary enforcement.
  Language-agnostic approach with concrete shell commands. Use for "audit code quality",
  "find dead code", "check test coverage", "review project structure", "find import
  violations", "check package boundaries", or "do a code health check".
---

# Codebase Audit

> **Load this skill for:** "audit code quality", "check project health",
> "find dead code", "review structure", "find import violations",
> "check package boundaries", "circular dependency audit".

Two complementary audit tracks:

1. **Structural quality** — dead code, duplicates, type hygiene, test gaps, oversized files
2. **Package boundaries** — import layering, circular deps, declared-vs-actual mismatches

Run them together for a full picture, or pick individual phases.

---

## Track A — Structural Quality Audit

Work through these phases in order. Each phase is self-contained — report
findings incrementally so nothing is lost if you stop early.

### A1 — Map the landscape

Get a bird's-eye view: folder structure, file counts, line counts, and test
coverage per module. This tells you where the mass is and where tests are missing.

```bash
# Per-module summary: lines | files | test files | name
for dir in src/*/; do
  name=$(basename "$dir")
  lines=$(find "$dir" -name '*.ts' -not -name '*.test.*' -not -name '*.spec.*' \
    | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}')
  files=$(find "$dir" -name '*.ts' -not -name '*.test.*' -not -name '*.spec.*' | wc -l)
  tests=$(find "$dir" \( -name '*.test.*' -o -name '*.spec.*' \) | wc -l)
  echo "$lines lines | $files files | $tests tests | $name"
done | sort -t'|' -k1 -rn
```

**Adapt:** Change `src/*/` to your project layout (`packages/*/`, `lib/*/`, `app/*/`).
Change `*.ts` to match your language (`*.py`, `*.go`, `*.rs`, `*.java`, `*.rb`).

**What to look for:**
- Modules with 0 tests — flag immediately
- Disproportionately large modules — candidates for splitting
- Empty or near-empty directories — potential dead directories

### A2 — Detect oversized files

Files beyond a threshold (300 lines is a common guideline) are harder to
review, test, and maintain. Surface them ranked by size.

```bash
THRESHOLD=300

find src/ -name '*.ts' -not -path '*/node_modules/*' \
  -not -name '*.test.*' -not -name '*.spec.*' -not -name '*.d.ts' |
while read f; do
  lines=$(wc -l < "$f")
  [ "$lines" -gt "$THRESHOLD" ] && echo "$lines $f"
done | sort -rn
```

### A3 — Find type safety violations

Scan for `any` types (TypeScript), untyped exceptions, and other type
system escape hatches. Exclude vendored code and type declaration files.

```bash
# TypeScript: any types (excluding node_modules, .d.ts, test files)
echo "=== any types ==="
rg -rn '\bany\b' src/ --glob '*.ts' \
  --glob '!node_modules/**' --glob '!*.d.ts' --glob '!*.test.*' |
  rg -E ':\s*any\b|as any\b|\bany\[' | head -40

# Untyped catch blocks (should be catch (err: unknown) in strict mode)
echo "=== untyped catches ==="
rg -rn 'catch\s*(err)' src/ --glob '*.ts' \
  --glob '!node_modules/**' --glob '!*.test.*'
```

**Python:** scan for bare `except:` or `except Exception:` without re-raise.
**Go:** scan for `_ = err` (discarded errors).
**Java:** scan for `catch (Exception e)` at overly broad scope.

### A4 — Find dead / duplicate code

This is the highest-value phase. Dead code is noise that confuses contributors
and wastes review cycles.

#### A4a — Structural convention violations

Check that files follow the project's naming/layout conventions (e.g.
folder-per-module, barrel exports, no loose single-files where folders are expected).

```bash
# Single files where folders are expected (adapt to your convention)
echo "=== single-file modules (should be folders) ==="
find src/ -maxdepth 1 -name '*.ts' -not -path '*/node_modules/*' | sort

# Modules missing an entry point (index.ts / __init__.py / mod.rs)
echo "=== modules missing entry point ==="
for dir in src/*/; do
  name=$(basename "$dir")
  [ -f "$dir/index.ts" ] || [ -f "$dir/src/index.ts" ] || \
    echo "MISSING: $name"
done

# Empty / ghost directories
echo "=== empty directories ==="
find src/ -type d -empty -not -path '*/node_modules/*'
```

#### A4b — Duplicate files

When code is refactored into a new layout, old files sometimes linger.
Compare names across old/new locations and check if they diverged.

```bash
# Find files with identical names in different directories
find src/ -name '*.ts' -not -path '*/node_modules/*' -not -name '*.test.*' |
  xargs -I{} basename {} | sort | uniq -d |
while read name; do
  echo "=== $name ==="
  find src/ -name "$name" -not -path '*/node_modules/*' -not -name '*.test.*'
done

# For each pair, check if identical or diverged
# diff -q file1 file2 → "Files ... are identical" or "Files ... differ"
```

#### A4c — Unreferenced files

The strongest signal: if nothing imports a file, it's dead. Check whether
your entry point (index.ts, main.ts, etc.) transitively reaches each file.

```bash
# For each source file, check if anything imports it
# (adapt the import pattern to your language)
find src/ -name '*.ts' -not -path '*/node_modules/*' -not -name '*.test.*' \
  -not -name 'index.ts' |
while read f; do
  base=$(basename "$f" .ts)
  # Check if any other file imports this module
  refs=$(rg -l -g '*.ts' -E "from.*['\"].*/${base}(\.js)?['\"]" src/ \
    -g '!node_modules/**' 2>/dev/null | rg -v "$f" | wc -l)
  [ "$refs" -eq 0 ] && echo "UNREFERENCED: $f"
done
```

### A5 — Scan for debt markers

```bash
rg -rn 'TODO\|FIXME\|HACK\|XXX\|WORKAROUND' src/ \
  -g '*.ts' -g '!node_modules/**' | head -30
```

Classify each hit: **actionable** (should be a ticket), **upstream blocker**
(document and leave), or **stale** (remove).

### A6 — Error handling consistency

```bash
# Catch blocks — check for untyped, bare, or swallowed exceptions
echo "=== catch blocks ==="
rg -rn 'catch\s*\(' src/ -g '*.ts' \
  -g '!node_modules/**' -g '!*.test.*' | head -20

# Empty catch blocks (swallowed errors)
rg -rn -A1 'catch' src/ -g '*.ts' \
  -g '!node_modules/**' -g '!*.test.*' |
  rg -B1 '^\s*}' | rg 'catch' | head -10
```

---

## Track B — Package Boundary Audit

> For monorepos with multiple packages/modules that should respect
> a dependency hierarchy.

Three categories of finding: **live violation** (real `import` crosses a forbidden
boundary), **comment false-positive** (forbidden name only in JSDoc/comments),
**doc gap** (`package.json` dep not reflected in architecture docs).

### B1 — Map the hierarchy from package.json

```bash
# Emit: package-name -> @scope/dep1, @scope/dep2   (adapt @your-scope)
for f in $(find . -name "package.json" -not -path "*/node_modules/*" | sort); do
  node -e "
    const p = require('$f');
    const deps = Object.keys({...(p.dependencies||{}), ...(p.devDependencies||{})})
      .filter(d => d.startsWith('@your-scope/'));
    if (p.name) console.log(p.name + ' -> ' + (deps.join(', ') || '(none)'));
  " 2>/dev/null
done
```

Read the output as layers — a package at layer N may only import from layers 0..N-1:

```
layer-0:  @scope/types       (no internal deps)
layer-1:  @scope/shared      (→ types)
layer-2:  @scope/graph       (→ shared, types)
layer-3:  @scope/core        (→ graph, shared, types)
```

### B2 — Scan for live import violations

**Key technique:** plain `rg` (ripgrep) hits JSDoc `@example` blocks and `//` comments.
The Perl negative lookahead `^(?!\s*[/*])` skips all comment lines.

```bash
SCOPE="@your-scope"

# Run all checks in parallel (no deps between them)
echo "=== types → anything above ==="
rg -rn -g "*.ts" \
  "^(?!\s*[/*]).*from ['\"]${SCOPE}/(shared|graph|core)" \
  packages/types/src/ || echo "(none)"

echo "=== shared → graph/core ==="
rg -rn -g "*.ts" \
  "^(?!\s*[/*]).*from ['\"]${SCOPE}/(graph|core)" \
  packages/shared/src/ || echo "(none)"

echo "=== graph → core ==="
rg -rn -g "*.ts" \
  "^(?!\s*[/*]).*from ['\"]${SCOPE}/core" \
  packages/graph/src/ || echo "(none)"
```

**Python:** swap `[/*]` → `#` and `from ['\"]` → `(?:import|from)\s+`
**Go:** swap `[/*]` → `//` and `from ['\"]` → `"`

Always confirm hits with `sed -n 'N-3,N+3p' FILE` before reporting.

### B3 — Check declared vs actual

```bash
PKG="packages/graph"; SCOPE="@your-scope"

# Phantom deps: declared in package.json but never imported
node -e "const p=require('./${PKG}/package.json');
  Object.keys({...(p.dependencies||{})}).filter(d=>d.startsWith('${SCOPE}/')).forEach(d=>console.log(d))
" | while read dep; do
  n=$(rg -rn -g "*.ts" "^(?!\s*[/*]).*from ['\"]${dep}" ${PKG}/src/ 2>/dev/null | wc -l)
  echo "$dep: $n live imports"
done

# Hidden deps: imported but not declared
rg -rhn -g "*.ts" \
  "^(?!\s*[/*]).*from ['\"](@your-scope/[^'\"]+)" \
  ${PKG}/src/ | rg -o "@your-scope/[^'\"]+" | sort -u
```

### B4 — Circular dependencies

```bash
# TS/JS — full transitive cycle detection
npx madge --circular --extensions ts packages/

# Python
pip install pydeps && pydeps src/my_package --show-cycles

# Relative path escapes (always a violation — bypasses all package management)
rg -rn -g "*.ts" -g "*.js" \
  "^(?!\s*[/*]).*from ['\"](\\.\./){3,}" packages/ | rg -v node_modules
```

---

## Report Template

```markdown
## Codebase Audit — <project> (<date>)

### Overall Grade: **X** — <one-line summary>

### ✅ Strengths
1. ...

### 🔴 Critical Issues
1. ...

### 🟡 Moderate Issues
1. ...

### 📊 By the Numbers

| Metric                   | Value |
|--------------------------|-------|
| Modules                  |       |
| Source lines (excl. vendored) |  |
| Dead/duplicate lines     |       |
| Test files               |       |
| Modules with zero tests  |       |
| Type escape hatches      |       |
| Files > threshold        |       |
| Debt markers (TODO etc.) |       |

### 🎯 Recommended Actions (priority order)
1. ...
```

---

## Adapt per project

| What | How |
|---|---|
| Source root | Replace `src/` with your layout (`packages/*/`, `lib/*/`, `extensions/*/`) |
| Language | `*.ts` → `*.py`, `*.go`, `*.rs`, `*.java`, `*.rb` etc. |
| File extension | `--include="*.ts"` → add `.tsx .js .mjs .py .go` as needed |
| Import pattern | `from ['"]` → `import ` (Python), `"` (Go), `use ` (Rust) |
| Comment char | `-P "^(?!\s*[/*])"` → `^(?!\s*#)` (Python), `^(?!\s*//)` (Go/Rust) |
| Entry point | `index.ts` → `__init__.py` (Python), `mod.rs` (Rust), `main.go` (Go) |
| Internal scope | Replace `@your-scope/` with your org prefix |
| Size threshold | 300 lines is a starting point — adjust to team preference |
| Test pattern | `*.test.*` / `*.spec.*` → `test_*.py`, `*_test.go` etc. |

**Pitfalls:**
- `import type` is erased at compile time (boundary-safe at runtime, still worth noting)
- `devDependencies` are non-runtime but test-file violations still signal coupling
- Barrel re-exports from a lower layer are fine if the package legitimately depends on it
- Dynamic imports (`import()`, `require()`) won't be caught by static rg (ripgrep) — use bundler analysis for full coverage
- In generated/vendored directories, all checks produce noise — always exclude them
