rigor-module-graph
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.
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:
-
include_constant_refs: true— emitconst_refedges from bare constant references inside method bodies. Off by default because the volume of edges grows fast on a typical Rails app and the noise can drown the structural picture. -
rails_zeitwerk: false— keep every edge atconfidence: "syntax"and skip the path-based owner inference. Useful when the project doesn’t follow Zeitwerk’s autoload convention.
Compatibility
-
Ruby
>= 4.0.0, < 4.1 -
rigortype
~> 0.2.1 -
rbs
~> 4.0
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.
-
API reference (GitHub Pages) — built from
main, mirrors the current source. -
API reference (RubyGems) — the last released gem on rubydoc.info.
-
How it works — the static-analysis pipeline (Prism → node rules → confidence ladder → JSONL → renderers), and why this is a nominal dependency graph and not a call graph.
-
Development guide — local setup, git hooks, CI / Release workflows, test suite layout.
-
Design plan — the decisions still load-bearing for the code (edge model, confidence ladder, output channel, owner resolution, architecture map).
-
Known limitations — rough edges shipped with the current release (visibility tracker gaps, the bundled inflector, Mermaid 10.x quirks).
-
Changelog — per-version changes, formatted per Keep a Changelog with Semantic Versioning. The release workflow gates on a
## [VERSION]entry being present before pushing to RubyGems.