class Rigor::ModuleGraph::CLI::View
view is the one-shot entry point: from the project root type rigor-module-graph and it analyses the current directory, writes a self-contained Mermaid HTML report, and opens it in a browser.
Defaults are tuned to need zero flags on a Rails-shaped project. The lower-level subcommands (collect / dot / mermaid) stay available for piped use.
Constants
- AUTO_COLLAPSE_THRESHOLD
-
An auto-collapsed cluster needs at least this many members before it’s worth folding. Three is the sweet spot empirically: a 1500-edge Rails app collapses into roughly the right shape, and a small fixture still leaves trivial Foo / Bar pairs uncollapsed.
- DEFAULT_HTML_OUTPUT
-
Default file destination when format is html and the user didn’t override with -o. Non-html formats default to stdout.
- DEFAULT_OUTPUT
- FORMATS
-
The supported output formats, in roughly increasing “wrapping” order.
htmlis the interactive Cytoscape viewer (vendored, self-contained);mermaid-htmlis the older static-Mermaid-via-CDN page kept for backwards compatibility;svgembeds the dot layout; the rest are raw text. - OPEN_WITH
-
--open-withflips the node-click action from clipboard copy to opening the file in an editor via a custom URL scheme.vscodeis the only supported editor today. - PATH_MODES
-
--path-modecontrols how the click-through metadatadata.pathis reported on every node. SeeViewer::Html#path_forfor what each mode emits. - SUBTITLE_COLLAPSE_PREVIEW
-
Cap the visible “collapsed: …” trailer in the subtitle so it doesn’t grow into an unreadable wall on large projects.
Public Class Methods
Source
# File lib/rigor/module_graph/cli.rb, line 392 def initialize(stdout:, stderr:) @stdout = stdout @stderr = stderr @options = { format: "html", output: nil, cache: false, quiet: false, rigor_cmd: ENV.fetch("RIGOR_CMD", "rigor"), open: true, collapse: nil, kinds: nil, confidences: nil, from: nil, depth: nil, direction: :both, edge_scope: :cluster, package: nil, include_methods: true, include_attributes: true, visibilities: %w[public protected private], path_mode: :relative, open_with: nil } end
Public Instance Methods
Source
# File lib/rigor/module_graph/cli.rb, line 655 def add_viewer_options(opts) opts.on("--path-mode MODE", PATH_MODES, "How to report node paths in the html viewer: " \ "#{PATH_MODES.join(" / ")} (default: relative). " \ "`none` strips path metadata entirely — useful when " \ "sharing the html artefact outside the project.") do |mode| @options[:path_mode] = mode end opts.on("--open-with EDITOR", OPEN_WITH, "Make node clicks open the file in EDITOR instead of " \ "copying path:line to the clipboard. " \ "Supported: #{OPEN_WITH.join(" / ")}.") do |editor| @options[:open_with] = editor end end
Source
# File lib/rigor/module_graph/cli.rb, line 464 def any_filter_active? @options[:kinds] || @options[:confidences] || @options[:from] || @options[:depth] end
Source
# File lib/rigor/module_graph/cli.rb, line 587 def build_parser OptionParser.new do |opts| opts.banner = "Usage: rigor-module-graph view [options] [PATHS...]" opts.on("--output FORMAT", FORMATS, "Output format (#{FORMATS.join("|")}; default: html). " \ "Non-html streams to stdout unless -o is given.") do |fmt| @options[:format] = fmt end opts.on("-o", "--save PATH", "Write to PATH instead of stdout / the default html location") do |path| @options[:output] = path end opts.on("--[no-]open", "Open the html in a browser (default: true; ignored for non-html)") do |flag| @options[:open] = flag end opts.on("--collapse PREFIXES", Array, "Manual collapse list (disables auto-detection)") do |prefixes| @options[:collapse] = prefixes end opts.on("--no-collapse", "Disable namespace collapse entirely") do @options[:collapse] = [] end opts.on("--no-methods", "[class-diagram] Don't render methods inside class bodies") do @options[:include_methods] = false end opts.on("--no-attributes", "[class-diagram] Don't render attributes inside class bodies") do @options[:include_attributes] = false end opts.on("--public-only", "[class-diagram] Only show public members") do @options[:visibilities] = %w[public] end opts.on("--no-private", "[class-diagram] Hide private members") do @options[:visibilities] = %w[public protected] end opts.on("--package", "Cluster by Packwerk packages discovered in cwd") do @options[:package] ||= "." end opts.on("--package-root PATH", "Cluster by Packwerk packages discovered under PATH") do |root| @options[:package] = root end opts.on("--[no-]cache", "Pass --cache / --no-cache to rigor (default: --no-cache)") do |cache| @options[:cache] = cache end opts.on("--rigor-cmd CMD", "Override the rigor binary (default: rigor or $RIGOR_CMD)") do |cmd| @options[:rigor_cmd] = cmd end opts.on("-q", "--quiet", "Suppress step-level progress on stderr") do @options[:quiet] = true end add_viewer_options(opts) add_filter_options(opts, @options) opts.on("-h", "--help") do @stdout.puts opts exit 0 end end end
Source
# File lib/rigor/module_graph/cli.rb, line 551 def deliver(payload, binary:, edges:, status: silent_status) destination = effective_output_path if destination.nil? if binary @stdout.binmode end @stdout.write(payload) return end status.step("Writing #{destination}") do dir = File.dirname(destination) FileUtils.mkdir_p(dir) unless dir.empty? || dir == "." mode = binary ? "wb" : "w" File.open(destination, mode) { |io| io.write(payload) } end @stderr.puts "rigor-module-graph: wrote #{edges.size} edge(s) to #{destination}" return unless html? && @options[:open] status.step("Opening #{destination} in browser") { open_in_browser(destination) } end
Writes the payload to the configured destination and opens the browser when the html-default flow applies. status: defaults to a silent reporter so the existing test surface (which exercises deliver directly) keeps working without threading a reporter through.
Source
# File lib/rigor/module_graph/cli.rb, line 675 def effective_collapse(edges) return @options[:collapse] unless @options[:collapse].nil? counts = Hash.new { |h, k| h[k] = Set.new } edges.each do |edge| [edge.from, edge.to].each do |name| head, tail = name.split("::", 2) # Only collapse on the top-level segment so a deep # tree like `Billing::Invoice::Line` still feeds into # the `Billing` cluster — picking inner prefixes # would compete with each other and produce nested # clusters that hurt readability. next if tail.nil? || tail.empty? # Absolute paths (`::Foo::Bar`) split with an empty # head; skip them so they don't surface as the bogus # `""` collapse target. next if head.empty? counts[head] << name end end counts.select { |_, members| members.size >= AUTO_COLLAPSE_THRESHOLD }.keys.sort end
Choose collapse prefixes. Explicit --collapse wins; otherwise we auto-pick top-level namespaces that have at least AUTO_COLLAPSE_THRESHOLD distinct nodes under them, which is what most graphs benefit from.
Source
# File lib/rigor/module_graph/cli.rb, line 576 def effective_output_path return @options[:output] if @options[:output] return DEFAULT_HTML_OUTPUT if html? nil end
Resolve the output path. -o PATH always wins. With no explicit path, html falls back to .rigor/module_graph/ view.html; every other format streams to stdout.
Source
# File lib/rigor/module_graph/cli.rb, line 534 def graphviz_svg(dot_source) stdout_str, stderr_str, status = Open3.capture3("dot", "-Tsvg", stdin_data: dot_source) unless status.success? raise RenderError, "graphviz `dot` failed (exit #{status.exitstatus}): #{stderr_str}" end stdout_str rescue Errno::ENOENT raise RenderError, "graphviz `dot` not found on PATH; install via " \ "`brew install graphviz` (macOS) or your distro's package manager" end
Shell out to Graphviz dot -Tsvg. Surfacing the binary check as a clear error keeps the message friendlier than the raw Errno::ENOENT Open3 would propagate.
Source
# File lib/rigor/module_graph/cli.rb, line 583 def html? %w[html mermaid-html].include?(@options[:format]) end
Source
# File lib/rigor/module_graph/cli.rb, line 737 def open_in_browser(path) opener = ENV["BROWSER"] || (RUBY_PLATFORM.include?("darwin") ? "open" : "xdg-open") system(opener, path) rescue StandardError => e @stderr.puts "rigor-module-graph view: could not open #{path}: #{e.message}" end
Source
# File lib/rigor/module_graph/cli.rb, line 724 def package_groups(edges) return nil unless @options[:package] overlay = PackwerkOverlay.discover(@options[:package]) unless overlay.any? @stderr.puts "rigor-module-graph view: no package.yml found under " \ "#{@options[:package].inspect}; falling back to namespace collapse" return nil end overlay.groups_for(edges) end
Source
# File lib/rigor/module_graph/cli.rb, line 478 def render_payload(edges, nodes, collapse, groups) case @options[:format] when "html" html = Viewer::Html.render( edges: edges, nodes: restrict_nodes_to_edges(nodes, edges), title: "rigor-module-graph: #{File.basename(Dir.pwd)}", subtitle: render_subtitle(edges, collapse, groups), path_mode: @options[:path_mode], open_with: @options[:open_with] ) [html, false] when "mermaid-html" mermaid = Mermaid.render(edges, collapse: collapse, groups: groups) html = HtmlView.render( title: "rigor-module-graph: #{File.basename(Dir.pwd)}", subtitle: render_subtitle(edges, collapse, groups), mermaid_source: mermaid ) [html, false] when "mermaid" [Mermaid.render(edges, collapse: collapse, groups: groups), false] when "dot" [Dot.render(edges, collapse: collapse, groups: groups), false] when "svg" [graphviz_svg(Dot.render(edges, collapse: collapse, groups: groups)), true] when "class-diagram" [ Uml::ClassDiagram.render( edges, restrict_nodes_to_edges(nodes, edges), include_methods: @options[:include_methods], include_attributes: @options[:include_attributes], visibilities: @options[:visibilities] ), false ] end end
Builds the rendered payload for the chosen format and signals whether the bytes are binary (svg via Graphviz can return a non-UTF-8 image stream).
Source
# File lib/rigor/module_graph/cli.rb, line 699 def render_subtitle(edges, collapse, groups) parts = ["#{edges.size} edge(s) from #{Dir.pwd}"] if @options[:from] from_part = +"from: #{Array(@options[:from]).join(", ")}" from_part << " (depth=#{@options[:depth]})" if @options[:depth] from_part << " [#{@options[:direction]}]" unless @options[:direction] == :both parts << from_part end if groups uniq_packages = groups.values.uniq.sort preview = uniq_packages.first(SUBTITLE_COLLAPSE_PREVIEW) label = +"packages: #{preview.join(", ")}" if uniq_packages.size > preview.size label << " (+#{uniq_packages.size - preview.size} more)" end parts << label elsif !collapse.empty? preview = collapse.first(SUBTITLE_COLLAPSE_PREVIEW) label = +"collapsed: #{preview.join(", ")}" label << " (+#{collapse.size - preview.size} more)" if collapse.size > preview.size parts << label end parts.join(" · ") end
Source
# File lib/rigor/module_graph/cli.rb, line 523 def restrict_nodes_to_edges(nodes, edges) return nodes if edges.empty? visible = Set.new edges.each { |edge| visible << edge.from << edge.to } nodes.select { |node| visible.include?(node.owner) || visible.include?(node.name) } end
When the user narrows the edge set with --from / --kind / --confidence, the class diagram should only show classes that participate in those edges — otherwise every constant declared in the project still shows up as a body-less class. The filter is a no-op when the edge set already covers every node (no filters applied).
Source
# File lib/rigor/module_graph/cli.rb, line 459 def rigor_step_label(paths) target = paths.empty? ? "configured paths" : paths.join(", ") "Running rigor check on #{target}" end
Source
# File lib/rigor/module_graph/cli.rb, line 418 def run(argv) parser = build_parser paths = parser.parse(argv) status = Rigor::ModuleGraph::StatusReporter.new(stderr: @stderr, quiet: @options[:quiet]) runner = RigorRunner.new(rigor_cmd: @options[:rigor_cmd], cache: @options[:cache]) edges, nodes = status.step(rigor_step_label(paths)) { runner.analyse(paths) } status.info "#{edges.size} edge(s), #{nodes.size} node(s)" if any_filter_active? edges = status.step("Applying filters") do apply_filters( edges, kinds: @options[:kinds], confidences: @options[:confidences], from: @options[:from], depth: @options[:depth], direction: @options[:direction], edge_scope: @options[:edge_scope] ) end status.info "#{edges.size} edge(s) after filters" end groups = package_groups(edges) collapse = groups ? [] : effective_collapse(edges) payload, binary = status.step("Rendering #{@options[:format]}") do render_payload(edges, nodes, collapse, groups) end deliver(payload, binary: binary, edges: edges, status: status) 0 rescue OptionParser::ParseError => e @stderr.puts "rigor-module-graph view: #{e.message}" 2 rescue CollectError, RenderError => e @stderr.puts "rigor-module-graph view: #{e.message}" 1 end
Source
# File lib/rigor/module_graph/cli.rb, line 469 def silent_status Rigor::ModuleGraph::StatusReporter.new(stderr: @stderr, quiet: true) end