rigor-module-graph 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf3abcab20d49807ed210e09aedba79d88b5ef2bbb1d96c15a95d5d06cf8806d
4
- data.tar.gz: 5579a21f011ed0dddb58ed3c62dad92eb682920a3f5c92f5f9d63576d8b231ad
3
+ metadata.gz: cea3ca68d705ecbe0f3534c5b3374e7e1dfd0f01edb6a0b9e64ed4c775cb166d
4
+ data.tar.gz: 70d09cd49d66f118948f2ec142d4f7a6c068a6ce098cddf97b3dd7d997a507d9
5
5
  SHA512:
6
- metadata.gz: df1406e30ec2d1efcbb8f7fb884435f320fdc4911746189c5d9063f2c02980a458f4765c8a6707f8b229e0d03c8475bc8d0a153a95ee6c4fc3580da7e2a9456a
7
- data.tar.gz: 0de9b8e511b698d3d8af731086a1df08890e72eda20254629d75f707ded01f367f5d536742fd8352269f2362f0b4a0f4ce295c6ab3de21b6b31a7ffd73f01c4f
6
+ metadata.gz: 5fec0afc7142600d308187afb293484818e545e63d5e21c08221412e0bcbca98db5cbf6e1e30c3b45ad46a30459779dcfe816459b9009494c5fbd4d0265a0dea
7
+ data.tar.gz: 1795a811ff1b184945dc55fb6a5451d09f895b85b778d1d98145f5fae2ad5f5aa0822dbafed51fad143ae3c32a8b75a5f3beec55cefdd3e9164658aa6e25e019
data/CHANGELOG.md CHANGED
@@ -17,6 +17,45 @@ Categories:
17
17
 
18
18
  ## [Unreleased]
19
19
 
20
+ ## [0.1.2] — 2026-06-20
21
+
22
+ First release that exercises the full automated pipeline end
23
+ to end — Trusted Publishing + GitHub Release + asset upload
24
+ all drive off a single `gh workflow run release.yml` after the
25
+ tag is pushed.
26
+
27
+ ### Added
28
+
29
+ - `view` and `collect` now emit step-level progress on stderr:
30
+ `==> Running rigor check ...`, post-step counts (`18 edge(s),
31
+ 16 node(s)`), and inline elapsed time (`done (428ms)`).
32
+ TTY-aware — the start / done halves render inline on a
33
+ terminal, on separate lines for redirected output, so logs
34
+ stay grep-friendly. `-q` / `--quiet` suppresses the progress
35
+ output for scripted use; the final `wrote N edge(s) to ...`
36
+ summary line stays. Driven by a new `StatusReporter` class
37
+ pinned by `test/rigor/module_graph/status_reporter_test.rb`.
38
+
39
+ ### Changed
40
+
41
+ - README restructured along the install → getting started →
42
+ usage → configuration flow. The "How it works" walkthrough
43
+ (pipeline diagram + the "not a call graph" framing) moves to
44
+ `docs/how-it-works.md` so the README stays focused on
45
+ "what do I type". Configuration section now notes that
46
+ `.rigor.yml` is required (rigor reads it to discover the
47
+ plugin), with a two-line minimum example up top and the
48
+ fully-elaborated default form below.
49
+
50
+ ### Fixed
51
+
52
+ - RDoc generation now parses Markdown instead of RDoc syntax,
53
+ so `![alt](path)` images in `README.md` / `CHANGELOG.md` /
54
+ `docs/*.md` actually render. `Rake::Task[:rdoc]` is enhanced
55
+ to copy `examples/billing/graph.svg` (and any future
56
+ `RDOC_ASSET_PATHS` entries) into `doc/` so the generated site
57
+ resolves the relative image references the README uses.
58
+
20
59
  ## [0.1.1] — 2026-06-20
21
60
 
22
61
  First Action-driven publish. The 0.1.0 release happened via the
@@ -134,6 +173,7 @@ spike through Phase 5 (UML class diagram).
134
173
  baseline and YJIT, and trailed baseline on Stats and
135
174
  CycleDetector — recommendation stays YJIT.
136
175
 
137
- [Unreleased]: https://github.com/nozomemein/rigor-module-graph/compare/v0.1.1...HEAD
176
+ [Unreleased]: https://github.com/nozomemein/rigor-module-graph/compare/v0.1.2...HEAD
177
+ [0.1.2]: https://github.com/nozomemein/rigor-module-graph/compare/v0.1.1...v0.1.2
138
178
  [0.1.1]: https://github.com/nozomemein/rigor-module-graph/compare/v0.1.0...v0.1.1
139
179
  [0.1.0]: https://github.com/nozomemein/rigor-module-graph/releases/tag/v0.1.0
data/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # rigor-module-graph
2
2
 
3
+ [![Gem Version](https://img.shields.io/gem/v/rigor-module-graph.svg)](https://rubygems.org/gems/rigor-module-graph)
4
+ [![License: MIT](https://img.shields.io/github/license/nozomemein/rigor-module-graph.svg)](LICENSE.txt)
5
+ [![CI](https://github.com/nozomemein/rigor-module-graph/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/nozomemein/rigor-module-graph/actions/workflows/ci.yml)
6
+ [![Docs](https://github.com/nozomemein/rigor-module-graph/actions/workflows/docs.yml/badge.svg?branch=main)](https://github.com/nozomemein/rigor-module-graph/actions/workflows/docs.yml)
7
+
3
8
  Class/module/constant dependency graph for Ruby projects, built on
4
9
  [Rigor](https://rigor.typedduck.fail/). The class-level counterpart
5
10
  to Packwerk/Graphwerk: where those look at package boundaries, this
@@ -11,54 +16,7 @@ looks at the Ruby nominal graph — inheritance, `include`/`prepend`/
11
16
  The screenshot above is from `examples/billing/`. Open
12
17
  `examples/billing/index.html` for the live Mermaid version.
13
18
 
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
19
+ ## Install
62
20
 
63
21
  Via Bundler:
64
22
 
@@ -71,65 +29,61 @@ gem "rigor-module-graph"
71
29
  bundle install
72
30
  ```
73
31
 
74
- Or globally:
32
+ Or system-wide:
75
33
 
76
34
  ```sh
77
35
  gem install rigor-module-graph
78
36
  ```
79
37
 
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
38
+ Both paths pull in `rigortype` and `rbs ~> 4.0` transitively.
39
+ The `rbs ~> 4.0` constraint is the one that matters: rigortype
40
+ 0.2.x calls `RBS::Environment::ClassEntry#each_decl`, which
41
+ only exists in rbs 4.x. The Ruby 4.0 stdlib bundles rbs 3.10
42
+ as a default gem, so installing `rigor-module-graph` (which
43
+ depends on rbs 4.x) makes RubyGems activate the 4.x gem at
44
+ run time and the analyzer stays alive.
89
45
 
90
- Add the plugin to your project's `.rigor.yml`:
46
+ For the full pipeline you also want `graphviz` installed so
47
+ `view --output svg` and `dot -Tsvg` can render PNG / SVG from
48
+ the generated DOT:
91
49
 
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
50
+ ```sh
51
+ brew install graphviz # macOS
52
+ apt-get install graphviz # Debian / Ubuntu
111
53
  ```
112
54
 
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.
55
+ A working `dot` on `$PATH` is optional text / Mermaid / HTML
56
+ output paths don't need it. See [How it works](docs/how-it-works.md)
57
+ for the pipeline overview.
117
58
 
118
- ## Usage
119
-
120
- ### One-shot: `view`
59
+ ## Getting started
121
60
 
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.
61
+ The default subcommand analyses the current directory, writes
62
+ a self-contained Mermaid HTML report under
63
+ `.rigor/module_graph/`, and opens it in a browser:
126
64
 
127
65
  ```sh
128
66
  cd path/to/your/project
129
67
  bundle exec rigor-module-graph # same as: rigor-module-graph view
130
68
  ```
131
69
 
132
- Useful flags:
70
+ A `.rigor.yml` must exist in the project root — that's how
71
+ `rigor` knows to load this plugin. The minimal version is two
72
+ lines:
73
+
74
+ ```yaml
75
+ plugins:
76
+ - gem: rigor-module-graph
77
+ ```
78
+
79
+ That's enough for `view` to run with all defaults. Everything
80
+ else (`paths:`, `autoload_paths:`, …) goes in the
81
+ [Configuration](#configuration) section below, and every key
82
+ defaults to a sensible Rails-shaped value.
83
+
84
+ ## Usage
85
+
86
+ ### `view` — one-shot HTML / SVG / Mermaid
133
87
 
134
88
  ```sh
135
89
  # Don't open the browser (just write the HTML)
@@ -164,7 +118,7 @@ rigor-module-graph view --package
164
118
  rigor-module-graph view --package-root /path/to/repo
165
119
  ```
166
120
 
167
- `--direction` controls how the +--from+ walk follows edges:
121
+ `--direction` controls how the `--from` walk follows edges:
168
122
 
169
123
  | direction | meaning |
170
124
  |-----------|----------------------------------------|
@@ -179,17 +133,19 @@ rigor-module-graph view --package-root /path/to/repo
179
133
  | `cluster` | keep every edge whose endpoints both fall in the reachable set (default — good for "show me the Article neighbourhood as a cluster") |
180
134
  | `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
135
 
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.
136
+ A 1-hop `--from Article --direction out --edge-scope walk`
137
+ returns exactly the edges whose `from` is `Article`, never the
138
+ sibling `inherits ApplicationRecord` of a reached node.
185
139
 
186
140
  ### Lower-level pipeline
187
141
 
188
- The pipeline `view` runs is also exposed as discrete subcommands
189
- when you want JSONL on disk or a pipeable text output:
142
+ The pipeline `view` runs is also exposed as discrete
143
+ subcommands when you want JSONL on disk or a pipeable text
144
+ output:
190
145
 
191
146
  ```sh
192
- # Run `rigor check` and write edges JSONL (default: .rigor/module_graph/edges.jsonl)
147
+ # Run `rigor check` and write edges JSONL
148
+ # (default: .rigor/module_graph/edges.jsonl)
193
149
  bundle exec rigor-module-graph collect
194
150
 
195
151
  # Render the graph
@@ -210,18 +166,19 @@ bundle exec rigor-module-graph class-diagram .rigor/module_graph/edges.jsonl > c
210
166
  bundle exec rigor-module-graph class-diagram --no-private --no-attributes edges.jsonl
211
167
  ```
212
168
 
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.
169
+ `collect` shells out to `rigor check --format json --no-cache`
170
+ and filters diagnostics on
171
+ `source_family == "plugin.module-graph"` + `rule == "edge"`,
172
+ so re-running is deterministic and there's no on-disk
173
+ side-effect from the plugin itself.
217
174
 
218
175
  `dot` / `mermaid` / `cycles` accept a file argument or read stdin.
219
176
 
220
177
  ### Filters and collapse
221
178
 
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.
179
+ All reader subcommands accept the same filter flags. They prune
180
+ the edge set before rendering / detecting; the JSONL on disk is
181
+ untouched.
225
182
 
226
183
  ```sh
227
184
  # Drop noisy const_ref / unresolved edges
@@ -230,7 +187,8 @@ bundle exec rigor-module-graph dot --kind inherits,include,prepend,extend edges.
230
187
  # Only the edges we're sure about
231
188
  bundle exec rigor-module-graph dot --confidence syntax,zeitwerk,rigor_type edges.jsonl
232
189
 
233
- # Fold every Billing::* node into one cluster (Dot subgraph_cluster_; Mermaid subgraph)
190
+ # Fold every Billing::* node into one cluster
191
+ # (Dot: subgraph_cluster_; Mermaid: subgraph)
234
192
  bundle exec rigor-module-graph dot --collapse Billing,Auth edges.jsonl
235
193
  bundle exec rigor-module-graph mermaid --collapse Billing edges.jsonl
236
194
 
@@ -247,6 +205,46 @@ bundle exec rigor-module-graph mermaid --package-root /path/to/repo edges.jsonl
247
205
  bundle exec rigor-module-graph cycles --kind inherits,include edges.jsonl
248
206
  ```
249
207
 
208
+ ## Configuration
209
+
210
+ `.rigor.yml` lives in the project root and is **required** —
211
+ `rigor` reads it to discover this plugin. `rigor init` scaffolds
212
+ a `.rigor.dist.yml` you can rename, or write it by hand. The
213
+ two-line minimum from [Getting started](#getting-started) is
214
+ enough; the full form below is for tuning.
215
+
216
+ ```yaml
217
+ target_ruby: '4.0'
218
+ paths:
219
+ - app
220
+ - lib
221
+ plugins:
222
+ - gem: rigor-module-graph
223
+ config:
224
+ rails_zeitwerk: true
225
+ autoload_paths:
226
+ - app/models
227
+ - app/controllers
228
+ - app/services
229
+ - app/jobs
230
+ - lib
231
+ concern_dirs:
232
+ - app/models/concerns
233
+ - app/controllers/concerns
234
+ include_constant_refs: false
235
+ ```
236
+
237
+ Every key shown is the default. Two switches worth knowing:
238
+
239
+ - `include_constant_refs: true` — emit `const_ref` edges from
240
+ bare constant references inside method bodies. Off by default
241
+ because the volume of edges grows fast on a typical Rails app
242
+ and the noise can drown the structural picture.
243
+ - `rails_zeitwerk: false` — keep every edge at
244
+ `confidence: "syntax"` and skip the path-based owner
245
+ inference. Useful when the project doesn't follow Zeitwerk's
246
+ autoload convention.
247
+
250
248
  ## Edge format
251
249
 
252
250
  Each edge in the JSONL file looks like:
@@ -279,6 +277,10 @@ published to GitHub Pages on every push to `main`.
279
277
  built from `main`, mirrors the current source.
280
278
  - [API reference (RubyGems)](https://rubydoc.info/gems/rigor-module-graph) —
281
279
  the last released gem on rubydoc.info.
280
+ - [How it works](docs/how-it-works.md) — the static-analysis
281
+ pipeline (Prism → node rules → confidence ladder → JSONL →
282
+ renderers), and why this is a nominal dependency graph and
283
+ not a call graph.
282
284
  - [Development guide](docs/development.md) — local setup, git
283
285
  hooks, CI / Release workflows, test suite layout.
284
286
  - [Design plan](docs/plan.md) — the decisions still
@@ -16,6 +16,7 @@ require_relative "reachability"
16
16
  require_relative "stats"
17
17
  require_relative "packwerk_overlay"
18
18
  require_relative "html_view"
19
+ require_relative "status_reporter"
19
20
  require_relative "uml/class_diagram"
20
21
 
21
22
  module Rigor
@@ -255,6 +256,7 @@ module Rigor
255
256
  output: DEFAULT_EDGES_PATH,
256
257
  nodes_output: DEFAULT_NODES_PATH,
257
258
  cache: false,
259
+ quiet: false,
258
260
  rigor_cmd: ENV.fetch("RIGOR_CMD", "rigor")
259
261
  }
260
262
  end
@@ -263,11 +265,15 @@ module Rigor
263
265
  parser = build_parser
264
266
  paths = parser.parse(argv)
265
267
 
268
+ status = Rigor::ModuleGraph::StatusReporter.new(stderr: @stderr, quiet: @options[:quiet])
269
+
266
270
  ensure_output_dirs
267
271
  runner = RigorRunner.new(rigor_cmd: @options[:rigor_cmd], cache: @options[:cache])
268
- edges, nodes = runner.analyse(paths)
269
- write_edges(edges)
270
- write_nodes(nodes)
272
+ edges, nodes = status.step(rigor_step_label(paths)) { runner.analyse(paths) }
273
+ status.info "#{edges.size} edge(s), #{nodes.size} node(s)"
274
+ status.step("Writing #{@options[:output]}") { write_edges(edges) }
275
+ status.step("Writing #{@options[:nodes_output]}") { write_nodes(nodes) }
276
+
271
277
  @stderr.puts "rigor-module-graph: wrote #{edges.size} edge(s) to #{@options[:output]}, " \
272
278
  "#{nodes.size} node(s) to #{@options[:nodes_output]}"
273
279
  0
@@ -279,6 +285,13 @@ module Rigor
279
285
  1
280
286
  end
281
287
 
288
+ # Path-aware label so the user can see which paths Rigor
289
+ # is being pointed at when the step is slow.
290
+ def rigor_step_label(paths)
291
+ target = paths.empty? ? "configured paths" : paths.join(", ")
292
+ "Running rigor check on #{target}"
293
+ end
294
+
282
295
  def build_parser
283
296
  OptionParser.new do |opts|
284
297
  opts.banner = "Usage: rigor-module-graph collect [options] [PATHS...]"
@@ -298,6 +311,9 @@ module Rigor
298
311
  "Override the rigor binary (default: rigor or $RIGOR_CMD)") do |cmd|
299
312
  @options[:rigor_cmd] = cmd
300
313
  end
314
+ opts.on("-q", "--quiet", "Suppress step-level progress on stderr") do
315
+ @options[:quiet] = true
316
+ end
301
317
  opts.on("-h", "--help") do
302
318
  @stdout.puts opts
303
319
  exit 0
@@ -365,6 +381,7 @@ module Rigor
365
381
  format: "html",
366
382
  output: nil,
367
383
  cache: false,
384
+ quiet: false,
368
385
  rigor_cmd: ENV.fetch("RIGOR_CMD", "rigor"),
369
386
  open: true,
370
387
  collapse: nil,
@@ -385,22 +402,34 @@ module Rigor
385
402
  parser = build_parser
386
403
  paths = parser.parse(argv)
387
404
 
405
+ status = Rigor::ModuleGraph::StatusReporter.new(stderr: @stderr, quiet: @options[:quiet])
406
+
388
407
  runner = RigorRunner.new(rigor_cmd: @options[:rigor_cmd], cache: @options[:cache])
389
- edges, nodes = runner.analyse(paths)
390
- edges = apply_filters(
391
- edges,
392
- kinds: @options[:kinds],
393
- confidences: @options[:confidences],
394
- from: @options[:from],
395
- depth: @options[:depth],
396
- direction: @options[:direction],
397
- edge_scope: @options[:edge_scope]
398
- )
408
+ edges, nodes = status.step(rigor_step_label(paths)) { runner.analyse(paths) }
409
+ status.info "#{edges.size} edge(s), #{nodes.size} node(s)"
410
+
411
+ if any_filter_active?
412
+ edges = status.step("Applying filters") do
413
+ apply_filters(
414
+ edges,
415
+ kinds: @options[:kinds],
416
+ confidences: @options[:confidences],
417
+ from: @options[:from],
418
+ depth: @options[:depth],
419
+ direction: @options[:direction],
420
+ edge_scope: @options[:edge_scope]
421
+ )
422
+ end
423
+ status.info "#{edges.size} edge(s) after filters"
424
+ end
425
+
399
426
  groups = package_groups(edges)
400
427
  collapse = groups ? [] : effective_collapse(edges)
401
428
 
402
- payload, binary = render_payload(edges, nodes, collapse, groups)
403
- deliver(payload, binary: binary, edges: edges)
429
+ payload, binary = status.step("Rendering #{@options[:format]}") do
430
+ render_payload(edges, nodes, collapse, groups)
431
+ end
432
+ deliver(payload, binary: binary, edges: edges, status: status)
404
433
  0
405
434
  rescue OptionParser::ParseError => e
406
435
  @stderr.puts "rigor-module-graph view: #{e.message}"
@@ -410,6 +439,20 @@ module Rigor
410
439
  1
411
440
  end
412
441
 
442
+ def rigor_step_label(paths)
443
+ target = paths.empty? ? "configured paths" : paths.join(", ")
444
+ "Running rigor check on #{target}"
445
+ end
446
+
447
+ def any_filter_active?
448
+ @options[:kinds] || @options[:confidences] ||
449
+ @options[:from] || @options[:depth]
450
+ end
451
+
452
+ def silent_status
453
+ Rigor::ModuleGraph::StatusReporter.new(stderr: @stderr, quiet: true)
454
+ end
455
+
413
456
  class RenderError < StandardError; end
414
457
 
415
458
  # Builds the rendered payload for the chosen format and
@@ -475,7 +518,10 @@ module Rigor
475
518
 
476
519
  # Writes the payload to the configured destination and
477
520
  # opens the browser when the html-default flow applies.
478
- def deliver(payload, binary:, edges:)
521
+ # `status:` defaults to a silent reporter so the existing
522
+ # test surface (which exercises `deliver` directly) keeps
523
+ # working without threading a reporter through.
524
+ def deliver(payload, binary:, edges:, status: silent_status)
479
525
  destination = effective_output_path
480
526
  if destination.nil?
481
527
  if binary
@@ -485,12 +531,16 @@ module Rigor
485
531
  return
486
532
  end
487
533
 
488
- dir = File.dirname(destination)
489
- FileUtils.mkdir_p(dir) unless dir.empty? || dir == "."
490
- mode = binary ? "wb" : "w"
491
- File.open(destination, mode) { |io| io.write(payload) }
534
+ status.step("Writing #{destination}") do
535
+ dir = File.dirname(destination)
536
+ FileUtils.mkdir_p(dir) unless dir.empty? || dir == "."
537
+ mode = binary ? "wb" : "w"
538
+ File.open(destination, mode) { |io| io.write(payload) }
539
+ end
492
540
  @stderr.puts "rigor-module-graph: wrote #{edges.size} edge(s) to #{destination}"
493
- open_in_browser(destination) if html? && @options[:open]
541
+ return unless html? && @options[:open]
542
+
543
+ status.step("Opening #{destination} in browser") { open_in_browser(destination) }
494
544
  end
495
545
 
496
546
  # Resolve the output path. `-o PATH` always wins. With no
@@ -563,6 +613,9 @@ module Rigor
563
613
  "Override the rigor binary (default: rigor or $RIGOR_CMD)") do |cmd|
564
614
  @options[:rigor_cmd] = cmd
565
615
  end
616
+ opts.on("-q", "--quiet", "Suppress step-level progress on stderr") do
617
+ @options[:quiet] = true
618
+ end
566
619
  add_filter_options(opts, @options)
567
620
  opts.on("-h", "--help") do
568
621
  @stdout.puts opts
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module ModuleGraph
5
+ # Step-level progress reporter that prints to stderr.
6
+ #
7
+ # On a TTY the message + elapsed time render inline on a
8
+ # single line ("==> Running rigor check... done (4.32s)").
9
+ # When stderr is redirected (CI logs, piping into another
10
+ # command, `tee` to a file) both halves print on separate
11
+ # lines so the output stays line-oriented and grep-friendly.
12
+ #
13
+ # `quiet: true` silences every method; callers can wire a
14
+ # `--quiet` CLI flag through without litterring conditionals
15
+ # at each call site.
16
+ #
17
+ # Usage:
18
+ #
19
+ # status = StatusReporter.new(stderr: $stderr)
20
+ # edges = status.step("Running rigor check") do
21
+ # runner.edges_for(paths)
22
+ # end
23
+ # status.info "#{edges.size} edges"
24
+ class StatusReporter
25
+ def initialize(stderr:, quiet: false)
26
+ @stderr = stderr
27
+ @quiet = quiet
28
+ @tty = stderr.respond_to?(:tty?) && stderr.tty?
29
+ end
30
+
31
+ # Print a "==> message..." line, yield, then print the
32
+ # outcome ("done (Xms)" or "failed") with elapsed time.
33
+ # Returns whatever the block returns; re-raises on
34
+ # exception after printing the failure tail so callers can
35
+ # still rescue normally.
36
+ def step(message)
37
+ return yield if @quiet
38
+
39
+ start_step(message)
40
+ started_at = monotonic
41
+ begin
42
+ result = yield
43
+ rescue StandardError
44
+ finish_step("failed", monotonic - started_at)
45
+ raise
46
+ end
47
+ finish_step("done", monotonic - started_at)
48
+ result
49
+ end
50
+
51
+ # Print an informational line indented under the most
52
+ # recent step. Used for "2016 edges, 87 nodes" style
53
+ # post-step counts.
54
+ def info(message)
55
+ return if @quiet
56
+
57
+ @stderr.puts " #{message}"
58
+ end
59
+
60
+ private
61
+
62
+ def start_step(message)
63
+ prefix = "==> #{message}"
64
+ if @tty
65
+ @stderr.print "#{prefix}... "
66
+ @stderr.flush
67
+ else
68
+ @stderr.puts prefix
69
+ end
70
+ end
71
+
72
+ def finish_step(verb, elapsed)
73
+ duration = format_duration(elapsed)
74
+ if @tty
75
+ @stderr.puts "#{verb} #{duration}"
76
+ else
77
+ @stderr.puts " #{verb} #{duration}"
78
+ end
79
+ end
80
+
81
+ def monotonic
82
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
83
+ end
84
+
85
+ def format_duration(seconds)
86
+ if seconds < 1
87
+ "(#{(seconds * 1000).round}ms)"
88
+ else
89
+ "(#{seconds.round(2)}s)"
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -5,6 +5,6 @@ module Rigor
5
5
  # an overview and Rigor::ModuleGraph::CLI for the CLI surface.
6
6
  module ModuleGraph
7
7
  # The installed gem version.
8
- VERSION = "0.1.1"
8
+ VERSION = "0.1.2"
9
9
  end
10
10
  end
@@ -29,4 +29,5 @@ require_relative "rigor/module_graph/stats"
29
29
  require_relative "rigor/module_graph/packwerk_overlay"
30
30
  require_relative "rigor/module_graph/uml/class_diagram"
31
31
  require_relative "rigor/module_graph/html_view"
32
+ require_relative "rigor/module_graph/status_reporter"
32
33
  require_relative "rigor/module_graph/plugin"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rigor-module-graph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nozomi Hijikata
@@ -70,6 +70,7 @@ files:
70
70
  - lib/rigor/module_graph/plugin/rigor_plugin.rb
71
71
  - lib/rigor/module_graph/reachability.rb
72
72
  - lib/rigor/module_graph/stats.rb
73
+ - lib/rigor/module_graph/status_reporter.rb
73
74
  - lib/rigor/module_graph/templates/view.html.erb
74
75
  - lib/rigor/module_graph/uml/class_diagram.rb
75
76
  - lib/rigor/module_graph/version.rb