Skip to content

Repository files navigation

wraith — Rust + TS/JS codebase analyzer

Unified analyzer for Rust + TypeScript/JavaScript codebases. One binary, one workspace pass, JSON / JSONL / human output, pre-commit-friendly exit codes. TS/JS analysis delegates to fallow, vendored at vendor/fallow/.

Subcommands

commandwhat it does
wraith dead-codepub items with no references in the workspace
wraith unused-depsdeps in Cargo.toml with no use / path reference
wraith circular-depscrate-level + module-level cycles (Tarjan SCC)
wraith dupestoken-shingled fn-body clone detection
wraith healthcyclomatic + cognitive complexity hotspots
wraith boundariesmodule-import allow/deny rules
wraith fixsafe auto-remove of dead pub items + unused deps (dry-run by default; --apply writes)
wraith auditdead-code + unused-deps scoped to git-changed files (pre-commit gate)
wraith initscaffold .wraithrc.json; optional `--ci=github
wraith hooks installgit pre-commit + Claude Code hook
`wraith migrate --from clippydeny`
wraith watchre-run on file save; emits jsonl with batch-end markers
wraith refactor extract-fnextract a contiguous line range from an enclosing fn into a new fn
wraith reportrun all detectors and emit a markdown summary (paste into README / PR)
wraith deps duplicatescrates resolved at multiple versions in Cargo.lock, with caller attribution
wraith deps auditknown security advisories (shells out to optional cargo-audit)
wraith deps unused-featuresflag features = […] entries with no usage (v1 heuristic; emits verify-manually)
wraith deps sizebinary-size attribution per crate (shells out to optional cargo-bloat)

Exit codes: 0 no findings, 1 findings, 2 internal error, 64 missing optional binary.

The deps audit / deps size subcommands require third-party tooling on PATH: cargo install cargo-audit and cargo install cargo-bloat. They exit 64 with an install hint when missing — no vendored shim.

Architecture

wraith/
├── crates/
│ ├── wraith-core/ library — workspace + parse + graph + analyze + audit + new modules:
│ │ circular, dupes, health, boundaries, fix
│ └── wraith-cli/ binary — `wraith` + hooks, migrate, watch, fallow shim
├── vendor/
│ └── fallow/ git subtree (fallow-rs/fallow main, MIT) for TS/JS analysis
└── examples/
└── small-test-crate/ fixture used by tests
  • Parser (Rust): syn 2.x — portable, no rustc internals.
  • Workspace discovery: cargo_metadata.
  • Graph: name-based symbol+reference graph; cycles via petgraph.
  • TS/JS: dispatched to a fallow binary on PATH; findings wrapped into the unified Finding schema with source: "fallow".

Finding schema (v1)

All commands emit Finding records carrying a schema_version (currently 1), severity (info / warning / error), and a kind tagged union covering: dead-code, unused-dep, circular-dep, duplicate, complexity, boundary-violation, and external (the wrapper around fallow output).

Config — .wraithrc.json

{
"ignore": ["target", "node_modules", ".git"],
"allow_dead": [],
"allow_unused_deps": [],
"treat_pub_crate_as_internal": true,
"duplicates": { "min_tokens": 40, "similarity_threshold": 0.85 },
"complexity": { "cyclomatic": 15, "cognitive": 25 },
"boundaries": [
{ "from": "guarded_crate", "allow": ["public_api"], "deny": ["internal"] }
]
}

Resolution limits

Name-based resolver — intentionally not type-aware. Trade-offs are false-negatives, not false-positives:

  • A pub fn foo collides with any other foo in scope; if either is referenced, both look alive.
  • Macro-generated items aren't seen.
  • Items inside mod tests are skipped wholesale.
  • cfg(...) blocks other than cfg(test) are walked unconditionally.

A future MIR-backed mode (tied to a pinned nightly) could close the gap.

Vendored fallow

vendor/fallow/ is a git subtree of github.com/fallow-rs/fallow main, squashed. It is excluded from this workspace via [workspace] exclude because:

  • fallow's workspace uses resolver = "3" (Rust 2024 edition) and pins the toolchain to 1.95; folding it into wraith's resolver = "2" workspace would require dep-version alignment on ~20 oxc crates plus a stable-1.95 host.

For now, wraith-cli shells out to a fallow binary if one is on PATH and merges its JSON findings via wraith_core::report::FindingKind::External. When the host toolchain bumps to 1.95+ and we move to a workspace-per- language layout, the shim in crates/wraith-cli/src/fallow.rs should be replaced by direct calls into fallow-core / fallow-extract.

Pull updates:

git subtree pull --prefix=packages/wraith/vendor/fallow fallow main --squash

Run

cd packages/wraith
cargo build
./target/debug/wraith --help
./target/debug/wraith --root path/to/your/cargo-workspace dead-code
./target/debug/wraith --root . unused-deps --format json
./target/debug/wraith audit --exit-zero
./target/debug/wraith circular-deps
./target/debug/wraith dupes
./target/debug/wraith health
./target/debug/wraith fix --apply # writes
./target/debug/wraith watch # streams jsonl on save

Claude Code skill

A wraith skill ships with the repo at skills/wraith/. It teaches AI agents (and humans) when to reach for which wraith subcommand and how to compose them into refactor workflows.

Install for your Claude Code:

ln -sfn "$(pwd)/skills/wraith"~/.claude/skills/wraith

The skill is also a useful read on its own — SKILL.md is a decision tree mapping intents ("I need to find code", "this fn is too complex") to wraith commands, and rules/ has deep dives on navigation, refactoring, diff reports, and monorepo usage.

Wraith on wavelet — real-world demo

Wraith was developed against packages/wavelet — the workbooks motion-graphics renderer (~71k LOC, 242 source files, one large Rust crate). The session that built out wraith's full surface also ran wraith against wavelet to validate it. Here's what wraith report --since=<pre-session-commit> produced at the end:

Before / after diff

$ cd packages/wavelet
$ wraith report --since=9da581812
metricbefore (9da581812)after (HEAD)delta
Source files242242+0
Lines of code71,54271,376−166
Total findings8772−15
Findings resolved28
Findings introduced13
Findings unchanged59

Plus the raw git diff (path-scoped): 20 files changed, +245 / −412 lines.

What was resolved

detectorresolved
dead-code13 (run_batch fn + voices catalog + agent error codes + 1 unused-render-fn)
dupes (clusters)13 (audio MIME quartet, image MIME quintuple, read_region pair)
unused-deps1 (animato — declared but never imported)
health1 (run_turn cyclo 30 → under threshold via extract-into-helpers)

What this validates about wraith

claimproof on wavelet
Zero false positives on dead-codeAll 14 auto-removed items kept the build green
Cargo.toml [package] never touched by fix --applyPre/post diff confirms section byte-identical (regression test wb-5lgj.21)
Resolver handles real crate-level patternswavelet's 71k LOC produces only 2 real cycles after the leaf-name disambiguation in wb-5lgj.24 — before that fix, the same crate produced a phantom 148-node SCC
Dupes detector finds structural clones, not just text-identicalassert_color_band_mean cluster (3 fns at 0.86–0.94 sim) is a real shader-assert boilerplate pattern; looks_like_image and looks_like_video were flagged as byte-identical but refused safely (scope-aware: they reference different SUPPORTED_EXTS) — caught the false positive wb-5lgj.37 from real usage
Diff reports actually narrate sessionsThe table above was produced by wraith report --since=<ref>. No manual aggregation.

Wraith found bugs in itself (caught via self-use during this session)

bugseverityhow it surfacedstatus
circular-deps 148-node phantom SCC (synthetic "crate" root)P2wraith ran against waveletfixed in wb-5lgj.20
fix --apply destroys Cargo.toml [package]P1applying to wavelet broke the buildfixed in wb-5lgj.21
Resolver leaf-name false positives (.new() matching across modules)P2second wave of circular-deps FPsfixed in wb-5lgj.24
dedupe-cluster scope-blind on module-local constsP1wavelet looks_like_image/looks_like_videofixed in wb-5lgj.37
dedupe-cluster doesn't elevate visibility of canonicalP2wavelet extract_grounded_text pair (canonical was private)fixed in wb-5lgj.38
suggest-extractions emits ranges extract-fn v1 refusesP2wavelet run_image (match-arm-bodies)fixed in wb-5lgj.39

Six real bugs caught by eating our own dog food on a real codebase, all fixed in the same session.

Wraith on the monorepo

Wraith analyzes one Cargo workspace at a time. Across this monorepo:

workspacecratessource filesLOCfindingstop finding
packages/wavelet/124271,37672run_image cyclo=145
packages/wraith/43717,99754self-found: 8 dead-code, 4 unused-deps, 14 dupes, 28 complexity
packages/orchestrator-core/1111,9652trivially clean — 1 dead-code, 1 dupe

Run wraith on any workspace by cd-ing into it, or with --root:

wraith --root packages/wraith report
wraith --root packages/wavelet report --since=main~10

See skills/wraith/rules/monorepo.md for patterns.

What gets restructured

When wraith identifies and the agent acts on its suggestions, the directory shape changes. From this session's wavelet work:

patternbeforeafter
MIME extension helpers8 local pick_ext / mime_to_ext / guess_mime fns scattered across backends/{google,fal,elevenlabs}/2 shared helpers (pick_image_ext_from_mime, pick_audio_ext_from_mime) in backends/util.rs
Region parsing2 near-identical read_region / read_region_yaml fns in validators/shader.rs1 shared region_from_floats helper + 2 thin format adapters
Agent turn loop1 god-function run_turn at 200 lines, cyclo=301 thin coordinator (65 lines) + 4 single-concern helpers (initialize_plan, initialize_system_prompt, check_step_termination, handle_text_response, dispatch_one_call)
Dead code14 unused pub items scattered (voices catalog, agent error codes, unused fn run_batch, etc.)gone

Wavelet's largest remaining files (the next refactor targets wraith identifies):

8007 src/bin/wavelet.rs ← god file; wraith health surfaces run_image cyclo=145 and 9 other handlers >50 cyclo
1607 src/css_filter.rs ← wraith hijack_filters_in_html @ cyclo=49 cog=126
1141 src/render_offline.rs
1150 src/agent/tools/plan_tools.rs
863 src/variants.rs

bin/wavelet.rs at 8000 lines is the obvious next break-up target. Wraith's refactor move-fn makes the work straightforward: each handler relocates to src/bin/handlers/<topic>.rs with all callers + use paths updated automatically.

Tests

cargo test --workspace

95+ tests passing across the wraith workspace — integration tests spawn synthetic Cargo workspaces in tempfile::tempdir()s, invoke the wraith binary, and assert on stdout. Plus unit tests in core crates and protocol smoke tests for wraith-lsp and wraith-mcp.

Test count growth tracked the feature growth:

  • MVP: 7 tests
  • After .4-.19 epic: 23 tests
  • After all .20-.39 agent-driven refactor tickets: 95 tests

Beads tickets

  • Epic: wb-5lgj
  • Closed (39): .1-.36 MVP + LSP/MCP/extract-fn-v1 + clustering + visibility + cache + graph queries + token-economy + dedupe-cluster + diff-cluster/extract-shared. .27 extract-fn v2, .31 move-fn/rename/inline/split-fn, .37 scope-aware dedupe, .38 visibility-elevation, .39 suggest-extractions feasibility filter.
  • All P1 bugs caught from self-use have been fixed.

About

Rust + TS/JS codebase analyzer — dead-code, dupes, cycles, complexity, agent-driven refactoring.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages