What it does
PhantomDep scans a repository and reports four classes of dependency defect that mainstream scanners ignore:
- Phantom — declared in a manifest, imported by zero source files.
- Undeclared — imported by source code but declared nowhere; it resolves only
because a parent hoisted it into
node_modules. - Cycle — a strongly connected component of size >= 2 in the lockfile graph.
- Tombstone — declared with
*,latest, or no version at all.
How it is built
Four languages, each doing the job it is genuinely good at, and zero third-party dependencies in any of them — a reviewer clones the repository and runs one script.
| Stage | Language | Responsibility |
|---|---|---|
| Orchestration | Go | manifest parsing, import extraction, HTTP API |
| Graph analysis | Rust | Tarjan SCC, depth, ghost scoring |
| Advisory intel | Python | licences and advisories, optional OSV.dev |
| Presentation | vanilla JS/SVG | layered dependency graph, no build step |
The split is functional rather than decorative: extraction is IO-bound and string-heavy,
so Go's filepath.WalkDir plus encoding/json covers it; analysis is pointer-chasing,
so Rust keeps the adjacency lists allocation-light; advisory payloads are nested and
irregular, so Python handles them in a few dozen lines. JSON is used only at the HTTP
boundaries — the Go to Rust hop is a versioned line protocol, because a dependency-free
JSON parser in Rust would be 200 lines of avoidable risk.
What is verified
Three fixtures, each documenting the exact defects it contains, so a reviewer can check
the claim instead of trusting it. scripts/verify.py runs the real CLI and fails the
build if any documented count drifts:
| Fixture | Phantom | Undeclared | Cycles |
|---|---|---|---|
haunted-node |
5 | 1 | 1 |
go-crypt |
2 | 0 | 0 |
py-ritual |
3 | 2 | 0 |
The Rust analyser carries its own unit tests for cycle detection, protocol validation and score bounds.
Known limitations
Stated rather than hidden, because a scanner that overstates its confidence is worse than no scanner:
- Python distribution names and import names diverge (
pyyamlvsyaml), so both verdicts are mechanically correct and useless to a human. This is left failing on purpose as a regression case, not papered over with a hardcoded alias table. - Dynamic imports (
require(variable),import(computed)) are invisible. - The ghost score is a heuristic, always rendered beside the raw counts that produced it.
- Cycle detection is npm-only today, because Go and Python have no lockfile edges.
Running it
./scripts/build.sh # builds Rust + Go, runs tests, verifies all fixtures
./scripts/dev.sh # starts the Python intel service and the UI on :8787
Requires Go 1.21+, Rust 1.70+, Python 3.9+. No npm install anywhere.
Log in or sign up for Devpost to join the conversation.