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.
The screenshot above is from examples/billing/. Open
examples/billing/index.html for the live Mermaid version.
What this actually does
In principle this is a static-analysis tool that turns Ruby source into a graph whose nodes are classes / modules / constants and whose edges are the references the language itself spells out.
The pipeline:
- Rigor parses Ruby into an AST with Prism.
- The plugin's
node_rules pick upClassNode/CallNode/ConstantReadNodeand friends. - Each interesting node becomes one or more edges:
class A < B→A -> B / inheritsinclude M→A -> M / include- a
Moneyconstant reference →A -> Money / const_ref(Phase 2 and later)
fromis the lexical owner, assembled by walkingcontext.ancestors— soclass Billing::InvoiceproducesBilling::Invoice, not justInvoice.tois resolved through a confidence ladder: syntax → Zeitwerk convention → Rigor type information. Whatever we couldn't pin down stays visible in theconfidencefield rather than being dropped.- Every edge ships as a Rigor
:infodiagnostic. Thecollectsubcommand filters them onrule == "edge"and writes JSONL. - DOT, SVG, Mermaid, and cycle detection are all derived from that JSONL.
So we are not watching what Ruby does at runtime. We're reading Ruby's named structure and reconstructing, approximately, "which constants depend on which other constants".
This is not a call graph
We do not track who foo.bar resolves to at runtime. We track
the fact that the Billing::Invoice name depends on the
ApplicationRecord / Auditable / Money names. That is a
nominal dependency graph — a compiler-front-end-style view
of the project's syntactic and lexical structure, projected into
edges with explicit confidence.
Not re-implementing Ruby constant lookup is deliberate. For
understanding a Rails codebase's shape, it's more useful to leave
each edge tagged syntax / zeitwerk / rigor_type /
unresolved than to fake a resolved answer and silently get it
wrong.
Installation
Via Bundler:
# Gemfile
gem "rigor-module-graph"
bundle install
Or globally:
gem install rigor-module-graph
Both paths pull in rigortype and rbs ~> 4.0 transitively. The
rbs ~> 4.0 constraint is the key one: 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.
Configuration
Add the plugin to your project's .rigor.yml:
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. Set include_constant_refs: true
to emit const_ref edges from constant references inside method
bodies. Set rails_zeitwerk: false to keep every edge at
confidence: "syntax" and skip path-based owner inference.
Usage
One-shot: view
The default subcommand analyses the current directory, writes a
self-contained Mermaid HTML report under .rigor/module_graph/,
and opens it in your browser. No flags needed for a Rails-shaped
project.
cd path/to/your/project
bundle exec rigor-module-graph # same as: rigor-module-graph view
Useful flags:
# 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 three 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
Edge format
Each edge in the JSONL file looks like:
{"from":"Billing::Invoice","to":"ApplicationRecord","kind":"inherits","path":"app/models/billing/invoice.rb","line":2,"column":3,"confidence":"syntax"}
kind:inherits/include/prepend/extend/const_ref(the last one is reserved for Phase 2).confidence:syntax/zeitwerk/rigor_type/unresolved. MVP only emitssyntax.
The renderers dedup by (from, to, kind, confidence) so two
include Foo on the same class across files collapse to one edge.
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.
- 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.