No description
  • Rust 68.3%
  • HTML 31.7%
Find a file
JMARyA 17273f86ec
docs: add README
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 23:16:25 +02:00
src feat: JSON/DOT/HTML renderers and CLI 2026-05-29 23:16:25 +02:00
.gitignore chore: scaffold cargo project 2026-05-29 23:16:24 +02:00
Cargo.lock chore: scaffold cargo project 2026-05-29 23:16:24 +02:00
Cargo.toml chore: scaffold cargo project 2026-05-29 23:16:24 +02:00
README.md docs: add README 2026-05-29 23:16:25 +02:00

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. couple makes the shape of a
codebase visible again.


What you get

  • A dependency graph of every function / struct / enum / trait in the tree.
  • Typed edgescalls, 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
    main and 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
(ChidamberKemerer, 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), in 0.0..1.0. 0 is a stable
    foundation (depended on, depends on little); 1 is 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 Graph is the source of truth. An Extractor
    produces one; everything downstream (metrics, rendering) is language-agnostic.
    Only a Rust extractor exists today — other languages slot in by producing the
    same Graph.
  • 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 an external node 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
    to external.
  • The standard library and other crates show up as external nodes. That red mass
    (clone, to_string, push, …) is expected noise — hide the external kind 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 / static access.
  • 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.