rigor-module-graph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5b9be2e8029422795b2b8b9e8f13998f6aefc16923e11e166f7692910637eb81
4
+ data.tar.gz: a723c08c549cb5368166de5971712f12d1cd5d06c142d176d842b43bd9ec6816
5
+ SHA512:
6
+ metadata.gz: e0ea19766f2fa753a0b41fe6dd7eb9b7840b3ce70ca10a8b1b153e2d04633f7cb0a04d1b24f6481104b038ab240e6d26fa56d6a4a990f404e565ce0ddfa21cdf
7
+ data.tar.gz: 82153c88b36de25bad1250d5f33c10b88d4a7c049d17112c750e4cf2596739407429f730048543e9c2bc7d0fe3551d877d6c6ee5275df780dceb4f1415742304
data/CHANGELOG.md ADDED
@@ -0,0 +1,118 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ Categories:
9
+
10
+ - **Added** — new functionality
11
+ - **Changed** — behaviour of existing functionality
12
+ - **Deprecated** — scheduled for removal
13
+ - **Removed** — gone
14
+ - **Fixed** — bug fixes
15
+ - **Security** — security fixes
16
+ - **Performance** — user-visible performance improvements
17
+
18
+ ## [Unreleased]
19
+
20
+ ## [0.1.0] — 2026-06-20
21
+
22
+ Initial release. Baseline shipping everything from the Phase 0
23
+ spike through Phase 5 (UML class diagram).
24
+
25
+ ### Added
26
+
27
+ - **Phase 0**: Rigor plugin API spike against `rigortype 0.2.1`
28
+ + `rbs ~> 4.0`, validating that `node_rule(Prism::ClassNode)`
29
+ and friends work and locking in the `:info` diagnostic output
30
+ channel.
31
+ - **Phase 1 (MVP)**: extraction of `inherits` / `include` /
32
+ `prepend` / `extend` edges. `Rigor::ModuleGraph::Edge` (a
33
+ `Data` subclass) with a JSONL writer, Graphviz DOT and Mermaid
34
+ flowchart renderers, and cycle detection via an iterative
35
+ Tarjan SCC.
36
+ - **Phase 2**: Zeitwerk-style path → constant inference
37
+ (`ZeitwerkResolver`), namespace collapse in both renderers
38
+ (DOT `subgraph cluster_*` and Mermaid `subgraph`), and
39
+ `const_ref` edges from constant references inside method
40
+ bodies (gated on `include_constant_refs`). Confidence promotes
41
+ from `syntax` → `zeitwerk` when the path-inferred name agrees
42
+ with the lexical owner.
43
+ - **Phase 3**: indirect mixin resolution via `scope.type_of`. A
44
+ `Rigor::Type::Singleton` carrier lifts the edge to
45
+ `confidence: "rigor_type"`; everything else degrades to
46
+ `"unresolved"` with the source slice preserved in `raw`. CLI
47
+ gains `--kind` and `--confidence` filters.
48
+ - **Phase 4**: `stats` subcommand reporting per-namespace
49
+ fan-in / fan-out / internal / nodes (text and JSON, with
50
+ `--grouping-depth N` and `--limit N`). Packwerk overlay
51
+ (`--package` / `--package-root PATH`) discovers `package.yml`
52
+ files recursively and uses them as the cluster boundary. The
53
+ Dot / Mermaid renderers accept an explicit `groups:` mapping
54
+ for arbitrary node → cluster assignments.
55
+ - **Phase 5**: UML-style class diagram. `collect` writes a
56
+ sibling `nodes.jsonl` covering class / module declarations,
57
+ method definitions, and `attr_*` attributes — with visibility
58
+ tracked via the `VisibilityMap`'s bare `private` / `protected`
59
+ / `public` keyword walk. Rails associations land as edges
60
+ (`has_many` / `belongs_to` / `has_one` /
61
+ `has_and_belongs_to_many`, with cardinality and a tiny
62
+ Rails-style inflector that maps `:invoices → Invoice`). A new
63
+ `Uml::ClassDiagram` renderer and `class-diagram` subcommand
64
+ emit Mermaid `classDiagram` syntax; filters `--no-methods`,
65
+ `--no-attributes`, `--public-only`, `--no-private`.
66
+ - **`view` one-shot subcommand**: `rigor-module-graph` with no
67
+ args (or `view` explicitly) analyses the current directory,
68
+ writes a self-contained HTML report under
69
+ `.rigor/module_graph/`, and opens it in a browser. The
70
+ `--output html|mermaid|dot|svg|class-diagram` flag switches
71
+ format; non-html streams to stdout unless `-o PATH` is given.
72
+ - **Reachability filter** (`--from NAMES`, `--depth N`,
73
+ `--direction in/out/both`) shared by every reader subcommand.
74
+ Subsequent `--edge-scope cluster|walk` flag distinguishes
75
+ "show the neighbourhood as a cluster" (default) from "show
76
+ only the edges the BFS actually traversed" (Codex review
77
+ confirmed naming and direction-both semantics).
78
+ - **Billing example** (`examples/billing/`): Customer /
79
+ Invoice / Payment / LineItem + concerns. `build.rb` runs the
80
+ same `view --output` pipeline that ships in the CLI, and
81
+ commits `index.html`, `class-diagram.html`, `graph.svg`, and
82
+ a `preview.png` so the GitHub view of the repo shows the
83
+ rendered output directly.
84
+ - **RDoc** support via `rake rdoc` / `rake rdoc:preview` /
85
+ `rake rdoc:server`.
86
+ - **minitest + minitest-snapshot** test harness. Snapshots
87
+ refresh with `UPDATE_SNAPSHOTS=1 rake test`.
88
+ - **SimpleCov C2 (branch) coverage** measurement via
89
+ `COVERAGE=1 rake test` or `rake coverage`. Baseline is 91.19%
90
+ branch coverage (445 / 488 branches).
91
+ - **lefthook** wiring rubocop / betterleaks / rigor / zizmor on
92
+ pre-commit and minitest on pre-push.
93
+ - **GitHub Actions**: `ci.yml` (test, lint, workflow-lint) and
94
+ `release.yml` (`workflow_dispatch`-only, uses RubyGems trusted
95
+ publishing — no long-lived API token).
96
+
97
+ ### Performance
98
+
99
+ - `Stats.compute` rewritten as a single pass with a mutable
100
+ per-namespace counter array instead of a `Data#with` cascade
101
+ per edge. On 2016 edges this took the call from **139 ms to
102
+ 47 ms (3.0×)**.
103
+ - `Reachability.walk` swaps `Set` for `Hash<name, true>` +
104
+ `Array` frontier, builds only the adjacency direction it
105
+ actually needs, and the `:both`-direction `walked_edge_indexes`
106
+ shares one pair of indexed adjacencies between its out-walk
107
+ and in-walk. Cluster depth-3 outbound: **27 ms → 14 ms (1.9×)**;
108
+ both direction: **35 ms → 19 ms (1.8×)**.
109
+ - `Edge#dedup_key` is now a generated Data member set once in
110
+ `Edge.build` as a `-`-frozen joined string. Renderer dedup
111
+ Hashes go from `Hash<Array,_>` to `Hash<String,_>`, which
112
+ drove the **1.2×** Dot / Mermaid render gain.
113
+ - YJIT (`--yjit`) adds another ~1.5×. ZJIT measured between
114
+ baseline and YJIT, and trailed baseline on Stats and
115
+ CycleDetector — recommendation stays YJIT.
116
+
117
+ [Unreleased]: https://github.com/nozomemein/rigor-module-graph/compare/v0.1.0...HEAD
118
+ [0.1.0]: https://github.com/nozomemein/rigor-module-graph/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nozomi Hijikata
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,294 @@
1
+ # rigor-module-graph
2
+
3
+ Class/module/constant dependency graph for Ruby projects, built on
4
+ [Rigor](https://rigor.typedduck.fail/). The class-level counterpart
5
+ to Packwerk/Graphwerk: where those look at package boundaries, this
6
+ looks at the Ruby nominal graph — inheritance, `include`/`prepend`/
7
+ `extend`, and (later) constant references.
8
+
9
+ ![billing example](examples/billing/graph.svg)
10
+
11
+ The screenshot above is from `examples/billing/`. Open
12
+ `examples/billing/index.html` for the live Mermaid version.
13
+
14
+ ## What this actually does
15
+
16
+ In principle this is a static-analysis tool that turns Ruby source
17
+ into a graph whose **nodes are classes / modules / constants** and
18
+ whose **edges are the references the language itself spells out**.
19
+
20
+ The pipeline:
21
+
22
+ 1. Rigor parses Ruby into an AST with Prism.
23
+ 2. The plugin's `node_rule`s pick up `ClassNode` / `CallNode` /
24
+ `ConstantReadNode` and friends.
25
+ 3. Each interesting node becomes one or more edges:
26
+ - `class A < B` → `A -> B / inherits`
27
+ - `include M` → `A -> M / include`
28
+ - a `Money` constant reference → `A -> Money / const_ref`
29
+ (Phase 2 and later)
30
+ 4. `from` is the lexical owner, assembled by walking
31
+ `context.ancestors` — so `class Billing::Invoice` produces
32
+ `Billing::Invoice`, not just `Invoice`.
33
+ 5. `to` is resolved through a confidence ladder: syntax →
34
+ Zeitwerk convention → Rigor type information. Whatever we
35
+ couldn't pin down stays visible in the `confidence` field
36
+ rather than being dropped.
37
+ 6. Every edge ships as a Rigor `:info` diagnostic. The `collect`
38
+ subcommand filters them on `rule == "edge"` and writes JSONL.
39
+ 7. DOT, SVG, Mermaid, and cycle detection are all derived from
40
+ that JSONL.
41
+
42
+ So we are not watching what Ruby *does at runtime*. We're reading
43
+ Ruby's *named structure* and reconstructing, approximately, "which
44
+ constants depend on which other constants".
45
+
46
+ ### This is not a call graph
47
+
48
+ We do not track who `foo.bar` resolves to at runtime. We track
49
+ the fact that the `Billing::Invoice` name depends on the
50
+ `ApplicationRecord` / `Auditable` / `Money` names. That is a
51
+ **nominal dependency graph** — a compiler-front-end-style view
52
+ of the project's syntactic and lexical structure, projected into
53
+ edges with explicit confidence.
54
+
55
+ Not re-implementing Ruby constant lookup is deliberate. For
56
+ understanding a Rails codebase's shape, it's more useful to leave
57
+ each edge tagged `syntax` / `zeitwerk` / `rigor_type` /
58
+ `unresolved` than to fake a `resolved` answer and silently get it
59
+ wrong.
60
+
61
+ ## Installation
62
+
63
+ Via Bundler:
64
+
65
+ ```ruby
66
+ # Gemfile
67
+ gem "rigor-module-graph"
68
+ ```
69
+
70
+ ```sh
71
+ bundle install
72
+ ```
73
+
74
+ Or globally:
75
+
76
+ ```sh
77
+ gem install rigor-module-graph
78
+ ```
79
+
80
+ Both paths pull in `rigortype` and `rbs ~> 4.0` transitively. The
81
+ `rbs ~> 4.0` constraint is the key one: rigortype 0.2.x calls
82
+ `RBS::Environment::ClassEntry#each_decl`, which only exists in
83
+ rbs 4.x. The Ruby 4.0 stdlib bundles rbs 3.10 as a default gem,
84
+ so installing `rigor-module-graph` (which depends on rbs 4.x)
85
+ makes RubyGems activate the 4.x gem at run time and the
86
+ analyzer stays alive.
87
+
88
+ ## Configuration
89
+
90
+ Add the plugin to your project's `.rigor.yml`:
91
+
92
+ ```yaml
93
+ target_ruby: '4.0'
94
+ paths:
95
+ - app
96
+ - lib
97
+ plugins:
98
+ - gem: rigor-module-graph
99
+ config:
100
+ rails_zeitwerk: true
101
+ autoload_paths:
102
+ - app/models
103
+ - app/controllers
104
+ - app/services
105
+ - app/jobs
106
+ - lib
107
+ concern_dirs:
108
+ - app/models/concerns
109
+ - app/controllers/concerns
110
+ include_constant_refs: false
111
+ ```
112
+
113
+ Every key shown is the default. Set `include_constant_refs: true`
114
+ to emit `const_ref` edges from constant references inside method
115
+ bodies. Set `rails_zeitwerk: false` to keep every edge at
116
+ `confidence: "syntax"` and skip path-based owner inference.
117
+
118
+ ## Usage
119
+
120
+ ### One-shot: `view`
121
+
122
+ The default subcommand analyses the current directory, writes a
123
+ self-contained Mermaid HTML report under `.rigor/module_graph/`,
124
+ and opens it in your browser. No flags needed for a Rails-shaped
125
+ project.
126
+
127
+ ```sh
128
+ cd path/to/your/project
129
+ bundle exec rigor-module-graph # same as: rigor-module-graph view
130
+ ```
131
+
132
+ Useful flags:
133
+
134
+ ```sh
135
+ # Don't open the browser (just write the HTML)
136
+ rigor-module-graph view --no-open
137
+
138
+ # Pick a different output format — html (default) opens a viewer
139
+ # in the browser; everything else streams to stdout unless `-o`
140
+ # is given.
141
+ rigor-module-graph view --no-open --output mermaid > graph.mmd
142
+ rigor-module-graph view --no-open --output dot > graph.dot
143
+ rigor-module-graph view --no-open --output svg > graph.svg
144
+ rigor-module-graph view --no-open --output class-diagram > class.mmd
145
+ rigor-module-graph view --output svg -o graph.svg
146
+
147
+ # Focus on what's around one or a few constants (Mermaid can't
148
+ # render 1000+-node graphs cleanly — this is the escape hatch)
149
+ rigor-module-graph view --from Article --depth 5
150
+ rigor-module-graph view --from Article --depth 5 --direction out
151
+ rigor-module-graph view --from Billing::Invoice,Billing::Payment --depth 2
152
+
153
+ # Pick your own collapse list (default: auto-detect top-level
154
+ # namespaces with ≥ 3 members)
155
+ rigor-module-graph view --collapse Billing,Auth
156
+ rigor-module-graph view --no-collapse
157
+
158
+ # Same kind / confidence filters as the lower-level commands
159
+ rigor-module-graph view --kind inherits,include
160
+ rigor-module-graph view --confidence syntax,zeitwerk
161
+
162
+ # Cluster by Packwerk packages (auto-detects package.yml under cwd)
163
+ rigor-module-graph view --package
164
+ rigor-module-graph view --package-root /path/to/repo
165
+ ```
166
+
167
+ `--direction` controls how the +--from+ walk follows edges:
168
+
169
+ | direction | meaning |
170
+ |-----------|----------------------------------------|
171
+ | `out` | only "what does Article depend on" |
172
+ | `in` | only "what depends on Article" |
173
+ | `both` | both (default) |
174
+
175
+ `--edge-scope` controls which edges survive once the BFS finishes:
176
+
177
+ | edge-scope | meaning |
178
+ |------------|------------------------------------------------------------|
179
+ | `cluster` | keep every edge whose endpoints both fall in the reachable set (default — good for "show me the Article neighbourhood as a cluster") |
180
+ | `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) |
181
+
182
+ A 1-hop `--from Article --direction out --edge-scope walk` returns
183
+ exactly the edges whose `from` is `Article`, never the sibling
184
+ `inherits ApplicationRecord` of a reached node.
185
+
186
+ ### Lower-level pipeline
187
+
188
+ The pipeline `view` runs is also exposed as discrete subcommands
189
+ when you want JSONL on disk or a pipeable text output:
190
+
191
+ ```sh
192
+ # Run `rigor check` and write edges JSONL (default: .rigor/module_graph/edges.jsonl)
193
+ bundle exec rigor-module-graph collect
194
+
195
+ # Render the graph
196
+ bundle exec rigor-module-graph dot .rigor/module_graph/edges.jsonl > graph.dot
197
+ bundle exec rigor-module-graph mermaid .rigor/module_graph/edges.jsonl > graph.mmd
198
+ dot -Tsvg graph.dot -o graph.svg
199
+
200
+ # Detect cycles (exit 1 if any are found)
201
+ bundle exec rigor-module-graph cycles .rigor/module_graph/edges.jsonl
202
+
203
+ # Per-namespace fan-in / fan-out report
204
+ bundle exec rigor-module-graph stats .rigor/module_graph/edges.jsonl
205
+ bundle exec rigor-module-graph stats --format json --limit 10 edges.jsonl
206
+
207
+ # UML class diagram (Mermaid classDiagram). Reads edges + the
208
+ # sibling nodes.jsonl that `collect` writes.
209
+ bundle exec rigor-module-graph class-diagram .rigor/module_graph/edges.jsonl > class.mmd
210
+ bundle exec rigor-module-graph class-diagram --no-private --no-attributes edges.jsonl
211
+ ```
212
+
213
+ `collect` shells out to `rigor check --format json --no-cache` and
214
+ filters diagnostics on `source_family == "plugin.module-graph"` +
215
+ `rule == "edge"`, so re-running is deterministic and there's no
216
+ on-disk side-effect from the plugin itself.
217
+
218
+ `dot` / `mermaid` / `cycles` accept a file argument or read stdin.
219
+
220
+ ### Filters and collapse
221
+
222
+ All three reader subcommands accept the same filter flags. They
223
+ prune the edge set before rendering / detecting; the JSONL on
224
+ disk is untouched.
225
+
226
+ ```sh
227
+ # Drop noisy const_ref / unresolved edges
228
+ bundle exec rigor-module-graph dot --kind inherits,include,prepend,extend edges.jsonl
229
+
230
+ # Only the edges we're sure about
231
+ bundle exec rigor-module-graph dot --confidence syntax,zeitwerk,rigor_type edges.jsonl
232
+
233
+ # Fold every Billing::* node into one cluster (Dot subgraph_cluster_; Mermaid subgraph)
234
+ bundle exec rigor-module-graph dot --collapse Billing,Auth edges.jsonl
235
+ bundle exec rigor-module-graph mermaid --collapse Billing edges.jsonl
236
+
237
+ # Restrict the graph to the neighbourhood of one or a few
238
+ # constants (works on dot / mermaid / cycles too)
239
+ bundle exec rigor-module-graph dot --from Article --depth 5 edges.jsonl
240
+ bundle exec rigor-module-graph mermaid --from Article --depth 5 --direction out edges.jsonl
241
+
242
+ # Cluster by Packwerk packages instead of by namespace
243
+ bundle exec rigor-module-graph dot --package edges.jsonl # cwd
244
+ bundle exec rigor-module-graph mermaid --package-root /path/to/repo edges.jsonl
245
+
246
+ # Cycles that stay within structural edges only
247
+ bundle exec rigor-module-graph cycles --kind inherits,include edges.jsonl
248
+ ```
249
+
250
+ ## Edge format
251
+
252
+ Each edge in the JSONL file looks like:
253
+
254
+ ```json
255
+ {"from":"Billing::Invoice","to":"ApplicationRecord","kind":"inherits","path":"app/models/billing/invoice.rb","line":2,"column":3,"confidence":"syntax"}
256
+ ```
257
+
258
+ - `kind`: `inherits` / `include` / `prepend` / `extend` /
259
+ `const_ref` (the last one is reserved for Phase 2).
260
+ - `confidence`: `syntax` / `zeitwerk` / `rigor_type` /
261
+ `unresolved`. MVP only emits `syntax`.
262
+
263
+ The renderers dedup by `(from, to, kind, confidence)` so two
264
+ `include Foo` on the same class across files collapse to one edge.
265
+
266
+ ## Compatibility
267
+
268
+ - Ruby `>= 4.0.0, < 4.1`
269
+ - rigortype `~> 0.2.1`
270
+ - rbs `~> 4.0`
271
+
272
+ ## Documentation
273
+
274
+ The public RDoc API is generated locally via `rake rdoc`,
275
+ served on `http://localhost:8808` via `rake rdoc:server`, and
276
+ published to GitHub Pages on every push to `main`.
277
+
278
+ - [API reference (GitHub Pages)](https://nozomemein.github.io/rigor-module-graph/) —
279
+ built from `main`, mirrors the current source.
280
+ - [API reference (RubyGems)](https://rubydoc.info/gems/rigor-module-graph) —
281
+ the last released gem on rubydoc.info.
282
+ - [Development guide](docs/development.md) — local setup, git
283
+ hooks, CI / Release workflows, test suite layout.
284
+ - [Design plan](docs/plan.md) — the decisions still
285
+ load-bearing for the code (edge model, confidence ladder,
286
+ output channel, owner resolution, architecture map).
287
+ - [Known limitations](docs/limitation.md) — rough edges shipped
288
+ with the current release (visibility tracker gaps, the
289
+ bundled inflector, Mermaid 10.x quirks).
290
+ - [Changelog](CHANGELOG.md) — per-version changes, formatted
291
+ per [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
292
+ with [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
293
+ The release workflow gates on a `## [VERSION]` entry being
294
+ present before pushing to RubyGems.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rigor/module_graph/version"
5
+ require "rigor/module_graph/cli"
6
+
7
+ exit(Rigor::ModuleGraph::CLI.run(ARGV.dup))