Read the Dependency Graph¶
jscan deps builds the module import graph and reports metrics derived from it. The numbers come from Robert Martin's package metrics, which are widely used but easy to misread. This guide explains what each one means and what to do about a bad value.
The vocabulary¶
Every metric is built from two counts. For a given module:
Afferent coupling, written Ca. The number of modules that import this one. High Ca means many things depend on you.
Efferent coupling, written Ce. The number of modules this one imports. High Ce means you depend on many things.
The two describe opposite kinds of exposure. A module with high Ca is dangerous to change, because the change ripples outward. A module with high Ce is fragile, because it breaks when anything it uses changes.
Instability¶
The result runs from 0 to 1.
An instability of 0 means nothing this module imports and many things import it. It is maximally stable: hard to change, safe to depend on. Type definitions and shared constants belong here.
An instability of 1 means it imports things and nothing imports it. It is maximally unstable: easy to change, and nothing breaks when you do. Application entry points and route handlers belong here.
Neither extreme is a problem in itself. The problem is a module in the wrong place. A stable module is only appropriate when its contents rarely need to change, so a stable module full of business rules is a liability, since every rule change forces a change to something many modules depend on.
The rule of thumb is that dependencies should point toward stability. A module should import things more stable than itself. When an unstable module is imported by a stable one, the stable module inherits the instability of what it depends on.
Abstractness and the main sequence¶
Abstractness measures how much of a module is interface rather than implementation. Instability measures how exposed it is to change. Plotting the two against each other gives a line from (0, 1) to (1, 0), called the main sequence, which represents a healthy balance: concrete modules should be unstable, abstract modules should be stable.
The main sequence deviation is the distance from that line, between 0 and 1. Zero is on the line and 1 is as far from it as possible.
The two bad corners are:
Abstract and unstable, at the top right. The module is full of interfaces that nothing uses. This is usually over-engineering, or the remains of an abstraction whose implementations were deleted.
Concrete and stable, at the bottom left. Many modules import a module full of concrete implementation. This is the more common and more painful case, since every change to that implementation touches everything downstream. The fix is to extract an interface and let dependents import that instead.
The deviation feeds the health score, contributing up to 3 penalty points. See the health score page.
Circular dependencies¶
A cycle is a group of modules that import each other, directly or through a chain. jscan finds them with Tarjan's strongly connected components algorithm, which finds every cycle in one pass.
Cycles matter for practical reasons rather than aesthetic ones. Module initialization order becomes undefined, so one module in the cycle sees a partially initialized version of another, which shows up as undefined at runtime in a way that is hard to trace. Bundlers cannot tree-shake across a cycle, so dead code survives into your bundle. Neither module can be understood or tested alone.
Dynamic imports written as import("./module") are excluded from cycle detection, because a dynamic import resolves at call time rather than at load time and therefore does not create a load-time cycle. This is often the cheapest way to break a cycle you cannot otherwise untangle.
Find them with:
jscan deps src/ --format json \
| jq -r '.analysis.circular_dependencies.circular_dependencies[].description'
Or fail the build on them:
The usual fix is to find the thing both modules need and move it into a third module that both import. A cycle between user.ts and order.ts usually means there is a shared type or helper wanting to live in its own file.
Depth¶
Maximum depth is the length of the longest import chain. jscan compares it against what a healthy graph of your size would have:
A graph of 100 modules is expected to be about 8 deep. Exceeding the expectation costs up to 3 points in the health score.
Deep chains make change expensive, because a modification at the bottom must be understood at every level above it. They usually come from layering that has grown one wrapper at a time.
Reading the DOT output¶
Nodes are colored green for low risk, yellow for medium, and red for high. Each node's tooltip carries its role, its Ca and Ce, and its instability. Modules that form a cycle are drawn inside a shared cluster, which makes cycles visible at a glance.
Large graphs are unreadable as a single image. Two flags help:
# Keep only the hubs
jscan deps src/ --dot --min-coupling 5 | dot -Tsvg -o hubs.svg
# Wide graphs read better horizontally
jscan deps src/ --dot --rank-dir LR | dot -Tsvg -o deps.svg
Rendering needs Graphviz, which provides the dot program. Install it with brew install graphviz, apt install graphviz, or the equivalent for your system.
A caution about path aliases¶
jscan does not resolve TypeScript path aliases. An import written @/lib/util becomes a module node of its own rather than resolving to src/lib/util.ts, which inflates the module count and drops the real edge.
Every metric on this page is computed from the graph, so every one of them is affected. In a project that uses aliases throughout, treat the coupling numbers as a lower bound and the cycle list as incomplete. Projects using relative imports get accurate results. The TypeScript guide covers this in detail.
A worked reading¶
$ jscan deps src/
Summary:
Total modules: 2
Total dependencies: 1
Root modules (entry points): 1
Leaf modules (no dependencies): 1
Max depth: 1
Coupling Analysis:
Average coupling: 1.00
Average instability: 0.50
Highly coupled modules: 0
Stable modules: 1
Two modules and one edge between them. One module imports nothing, so it is a leaf and a stable module with instability 0. The other imports it and nothing imports the other, so it is an entry point with instability 1. The average of 0 and 1 is the reported 0.50.
There are no cycles and the depth of 1 is well under the expected 3, so the only dependency penalty comes from the main sequence deviation.
See also¶
jscan depsreference for every flag- Health score for how these metrics affect the score