rigor-module-graph

Gem Version License: MIT CI Docs

Class/module/constant dependency graph for Ruby projects, built on Rigor. The class-level counterpart to Packwerk/Graphwerk: where those look at package boundaries, this looks at the Ruby nominal graph — inheritance, include/prepend/ extend, and (later) constant references.

billing example

The screenshot above is from examples/billing/. Open examples/billing/index.html for the live Mermaid version.

Install

Via Bundler:

# Gemfile
gem "rigor-module-graph"
bundle install

Or system-wide:

gem install rigor-module-graph

Both paths pull in rigortype and rbs ~> 4.0 transitively. The rbs ~> 4.0 constraint is the one that matters: rigortype 0.2.x calls RBS::Environment::ClassEntry#each_decl, which only exists in rbs 4.x. The Ruby 4.0 stdlib bundles rbs 3.10 as a default gem, so installing rigor-module-graph (which depends on rbs 4.x) makes RubyGems activate the 4.x gem at run time and the analyzer stays alive.

For the full pipeline you also want graphviz installed so view --output svg and dot -Tsvg can render PNG / SVG from the generated DOT:

brew install graphviz       # macOS
apt-get install graphviz    # Debian / Ubuntu

A working dot on $PATH is optional — text / Mermaid / HTML output paths don’t need it. See How it works for the pipeline overview.

Getting started

The default subcommand analyses the current directory, writes a self-contained Mermaid HTML report under .rigor/module_graph/, and opens it in a browser:

cd path/to/your/project
bundle exec rigor-module-graph         # same as: rigor-module-graph view

A .rigor.yml must exist in the project root — that’s how rigor knows to load this plugin. The minimal version is two lines:

plugins:
  - gem: rigor-module-graph

That’s enough for view to run with all defaults. Everything else (paths:, autoload_paths:, …) goes in the Configuration section below, and every key defaults to a sensible Rails-shaped value.

Usage

view — one-shot HTML / SVG / Mermaid

# Don't open the browser (just write the HTML)
rigor-module-graph view --no-open

# Pick a different output format — html (default) opens a viewer
# in the browser; everything else streams to stdout unless `-o`
# is given.
rigor-module-graph view --no-open --output mermaid       > graph.mmd
rigor-module-graph view --no-open --output dot           > graph.dot
rigor-module-graph view --no-open --output svg           > graph.svg
rigor-module-graph view --no-open --output class-diagram > class.mmd
rigor-module-graph view --output svg -o graph.svg

# Focus on what's around one or a few constants (Mermaid can't
# render 1000+-node graphs cleanly — this is the escape hatch)
rigor-module-graph view --from Article --depth 5
rigor-module-graph view --from Article --depth 5 --direction out
rigor-module-graph view --from Billing::Invoice,Billing::Payment --depth 2

# Pick your own collapse list (default: auto-detect top-level
# namespaces with ≥ 3 members)
rigor-module-graph view --collapse Billing,Auth
rigor-module-graph view --no-collapse

# Same kind / confidence filters as the lower-level commands
rigor-module-graph view --kind inherits,include
rigor-module-graph view --confidence syntax,zeitwerk

# Cluster by Packwerk packages (auto-detects package.yml under cwd)
rigor-module-graph view --package
rigor-module-graph view --package-root /path/to/repo

--direction controls how the --from walk follows edges:

direction meaning
out only “what does Article depend on”
in only “what depends on Article”
both both (default)

--edge-scope controls which edges survive once the BFS finishes:

edge-scope meaning
cluster keep every edge whose endpoints both fall in the reachable set (default — good for “show me the Article neighbourhood as a cluster”)
walk keep only the edges the BFS actually traversed (good for “show me what depends on Article and nothing else”; drops sibling edges like Foo inherits ApplicationRecord that just happen to share a base class with reachable nodes)

A 1-hop --from Article --direction out --edge-scope walk returns exactly the edges whose from is Article, never the sibling inherits ApplicationRecord of a reached node.

Lower-level pipeline

The pipeline view runs is also exposed as discrete subcommands when you want JSONL on disk or a pipeable text output:

# Run `rigor check` and write edges JSONL
# (default: .rigor/module_graph/edges.jsonl)
bundle exec rigor-module-graph collect

# Render the graph
bundle exec rigor-module-graph dot     .rigor/module_graph/edges.jsonl > graph.dot
bundle exec rigor-module-graph mermaid .rigor/module_graph/edges.jsonl > graph.mmd
dot -Tsvg graph.dot -o graph.svg

# Detect cycles (exit 1 if any are found)
bundle exec rigor-module-graph cycles  .rigor/module_graph/edges.jsonl

# Per-namespace fan-in / fan-out report
bundle exec rigor-module-graph stats   .rigor/module_graph/edges.jsonl
bundle exec rigor-module-graph stats --format json --limit 10 edges.jsonl

# UML class diagram (Mermaid classDiagram). Reads edges + the
# sibling nodes.jsonl that `collect` writes.
bundle exec rigor-module-graph class-diagram .rigor/module_graph/edges.jsonl > class.mmd
bundle exec rigor-module-graph class-diagram --no-private --no-attributes edges.jsonl

collect shells out to rigor check --format json --no-cache and filters diagnostics on source_family == "plugin.module-graph" + rule == "edge", so re-running is deterministic and there’s no on-disk side-effect from the plugin itself.

dot / mermaid / cycles accept a file argument or read stdin.

Filters and collapse

All reader subcommands accept the same filter flags. They prune the edge set before rendering / detecting; the JSONL on disk is untouched.

# Drop noisy const_ref / unresolved edges
bundle exec rigor-module-graph dot --kind inherits,include,prepend,extend edges.jsonl

# Only the edges we're sure about
bundle exec rigor-module-graph dot --confidence syntax,zeitwerk,rigor_type edges.jsonl

# Fold every Billing::* node into one cluster
# (Dot: subgraph_cluster_; Mermaid: subgraph)
bundle exec rigor-module-graph dot     --collapse Billing,Auth edges.jsonl
bundle exec rigor-module-graph mermaid --collapse Billing edges.jsonl

# Restrict the graph to the neighbourhood of one or a few
# constants (works on dot / mermaid / cycles too)
bundle exec rigor-module-graph dot     --from Article --depth 5 edges.jsonl
bundle exec rigor-module-graph mermaid --from Article --depth 5 --direction out edges.jsonl

# Cluster by Packwerk packages instead of by namespace
bundle exec rigor-module-graph dot     --package edges.jsonl  # cwd
bundle exec rigor-module-graph mermaid --package-root /path/to/repo edges.jsonl

# Cycles that stay within structural edges only
bundle exec rigor-module-graph cycles --kind inherits,include edges.jsonl

Configuration

.rigor.yml lives in the project root and is required —rigor reads it to discover this plugin. rigor init scaffolds a .rigor.dist.yml you can rename, or write it by hand. The two-line minimum from Getting started is enough; the full form below is for tuning.

target_ruby: '4.0'
paths:
  - app
  - lib
plugins:
  - gem: rigor-module-graph
    config:
      rails_zeitwerk: true
      autoload_paths:
        - app/models
        - app/controllers
        - app/services
        - app/jobs
        - lib
      concern_dirs:
        - app/models/concerns
        - app/controllers/concerns
      include_constant_refs: false

Every key shown is the default. Two switches worth knowing:

Compatibility

Documentation

The public RDoc API is generated locally via rake rdoc, served on http://localhost:8808 via rake rdoc:server, and published to GitHub Pages on every push to main.