- Rust 68.3%
- HTML 31.7%
|
|
||
|---|---|---|
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
couple
See how your code is actually wired.
couple reads a Rust source tree and builds a graph of its entities — functions,
structs, enums, traits — and the dependencies between them: what calls what, what
constructs what, what implements what. The result is a map you can explore in your
browser, where coupling hotspots, tangled orchestrators, and the load-bearing core
of your program become things you can see instead of guess at.
The problem it's built for: machines now write a lot of code, and it stops being
legible at scale. Architecture decisions get buried.couplemakes the shape of a
codebase visible again.
What you get
- A dependency graph of every function / struct / enum / trait in the tree.
- Typed edges —
calls,constructs,implements,method of,contains—
so you can look at behaviour and structure separately (see the model). - Coupling metrics baked into every node: fan-in, fan-out, instability.
- An interactive viewer (a single self-contained HTML file) where you start at
mainand expand outward, flag hotspots, and resize nodes by how depended-on they are. - Machine-readable JSON as the canonical output — everything else renders from it.
Install
Requires a recent Rust toolchain.
git clone <this-repo> couple
cd couple
cargo build --release
The binary lands at target/release/couple.
Usage
Point it at a crate or a workspace and pick an output format:
# Interactive graph you can open in a browser
couple . --format html --out graph.html
open graph.html
# Canonical JSON (the source of truth — feed it to other tools)
couple . --format json --out graph.json
# Graphviz DOT, for static rendering
couple . --format dot | dot -Tsvg -o graph.svg
couple [PATH] [--format json|dot|html] [--out FILE]
PATH crate or workspace root to analyze (default: ".")
-f, --format output format (default: json)
-o, --out write to a file instead of stdout
Every run also prints a quick coupling summary to stderr:
couple: 145 nodes, 322 edges (69 unresolved)
hotspots (most dependencies / fan-out):
28 couple::extract::rust::RustExtractor::extract
23 couple::extract::rust::discover_crates
most depended-on (fan-in):
9 couple::model::GraphBuilder
6 couple::model::GraphBuilder::add_edge
Reading the graph
The viewer
Open the generated graph.html. It starts focused on main (or your crate roots)
and lets you drill in:
| Control | What it does |
|---|---|
| Click a node | Reveal what it depends on. Click again to collapse. A dashed ring means "has more to expand." |
| Reveal depth slider | Open several hops out from the root at once. |
| Mode → Full graph | Show everything; hover to spotlight a node's connections, click to pin. |
| Start from | Re-root the exploration on any function or crate. |
| Size nodes by | Scale node size by total connections, fan-in, or fan-out. |
| Flag hotspots | Draw a red ring around the busiest units (top ~15% by fan-out). |
| Relationships toggles | Show/hide edge kinds. contains (the module tree) is off by default — it dominates everything. |
| Hover | Tooltip with the qualified path, source location, and metrics. |
Arrows always point from a thing to what it uses. Bigger circles are more
connected. Red nodes are unresolved references (usually the standard library).
The metrics
Each node carries three numbers, drawn from the classic coupling literature
(Chidamber–Kemerer, Robert Martin):
- fan-in (afferent coupling) — how many things depend on this. High fan-in =
load-bearing; changing it ripples widely. - fan-out (efferent coupling) — how many things this depends on. High fan-out =
a busy, tangled unit; the first place refactoring gets painful. - instability =
fan_out / (fan_in + fan_out), in0.0..1.0.0is a stable
foundation (depended on, depends on little);1is a fragile leaf.
Metrics are computed over dependency edges only — structural contains edges don't
count as coupling.
The graph model
couple separates node kinds from edge kinds. Edges are deliberately typed
rather than a single fuzzy "depends on", so the worst couplings can be told apart
from the benign ones — echoing the classic Stevens/Constantine taxonomy
(content → common → control → stamp → data coupling).
Node kinds: workspace, crate, module, function, struct, enum,
trait, type_alias, const, and external (an unresolved reference).
Edge kinds:
| Edge | Meaning |
|---|---|
contains |
Structural nesting: crate → module → item. |
calls |
A function calls another function or method. |
instantiates |
A function constructs / uses a type (Foo { .. }, Foo::new(..)). |
implements |
A type implements a trait. |
method_of |
A function belongs to a type or trait. |
Modelled but not yet extracted:
field_type,signature,variant_type
(type-level coupling). See Roadmap.
The JSON output is just { "nodes": [...], "edges": [...] }, with each node carrying
its id, kind, qualified path, source location, and metrics.
How it works
source tree ──▶ extractor ──▶ graph (neutral) ──▶ analyze ──▶ renderers
(syn, per-lang) nodes + edges (metrics) json · dot · html
- Language-neutral core. The
Graphis the source of truth. AnExtractor
produces one; everything downstream (metrics, rendering) is language-agnostic.
Only a Rust extractor exists today — other languages slot in by producing the
sameGraph. - Syntactic extraction via
syn. No compilation. Fast,
but purely syntactic — see limitations. - Two-pass resolution. Pass 1 walks every file and records every definition into
a symbol table. Pass 2 re-walks bodies and resolves call sites against it. Anything
it can't resolve becomes anexternalnode rather than being dropped — so the lossy
fraction stays visible.
Source layout:
src/
model.rs # the neutral graph: Node, Edge, Graph, metrics
analyze.rs # fan-in / fan-out / instability
extract.rs # the Extractor trait
extract/rust.rs # the syn-based Rust extractor
render/ # json, dot, and the html viewer
cli.rs, main.rs # the command line
Limitations
couple is syntactic, by design. That buys speed and zero build setup, at a cost:
- Trait dispatch and generics are lossy. A
x.method()call is resolved by name;
if several types have a method of that name, it may resolve ambiguously or fall back
toexternal. - The standard library and other crates show up as
externalnodes. That red mass
(clone,to_string,push, …) is expected noise — hide theexternalkind to
focus on your own code. - Rust only, for now.
A semantic backend (rust-analyzer / rustc HIR) would resolve these precisely, at much
higher cost. The neutral graph model is designed to accommodate it later without
disturbing the renderers.
Roadmap
- Type-level coupling edges:
field_type,signature,variant_type. - Common coupling: shared global /
staticaccess. - Module-level rollup view (boxes for modules, weighted arrows between them).
- More languages: Python, Nix.
- Beyond structural coupling: logical (git co-change) and semantic
(identifier/comment similarity) dimensions.
License
TBD.