Class: Rigor::ModuleGraph::CLI::View
- Inherits:
-
Object
- Object
- Rigor::ModuleGraph::CLI::View
- Includes:
- EdgeFilters
- Defined in:
- lib/rigor/module_graph/cli.rb
Overview
‘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.
Defined Under Namespace
Classes: RenderError
Constant Summary collapse
- DEFAULT_OUTPUT =
".rigor/module_graph/view.html"- 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.
3- SUBTITLE_COLLAPSE_PREVIEW =
Cap the visible “collapsed: …” trailer in the subtitle so it doesn’t grow into an unreadable wall on large projects.
6- FORMATS =
The supported output formats, in roughly increasing “wrapping” order. ‘html` is the interactive Cytoscape viewer (vendored, self-contained); `mermaid-html` is the older static-Mermaid-via-CDN page kept for backwards compatibility; `svg` embeds the dot layout; the rest are raw text.
%w[html mermaid-html mermaid dot svg class-diagram].freeze
- PATH_MODES =
‘–path-mode` controls how the click-through metadata `data.path` is reported on every node. See `Viewer::Html#path_for` for what each mode emits.
%i[relative absolute none].freeze
- OPEN_WITH =
‘–open-with` flips the node-click action from clipboard copy to opening the file in an editor via a custom URL scheme. `vscode` is the only supported editor today.
%i[vscode].freeze
- DEFAULT_HTML_OUTPUT =
Default file destination when format is html and the user didn’t override with -o. Non-html formats default to stdout.
".rigor/module_graph/view.html"
Constants included from EdgeFilters
EdgeFilters::VALID_CONFIDENCES, EdgeFilters::VALID_DIRECTIONS, EdgeFilters::VALID_EDGE_SCOPES, EdgeFilters::VALID_KINDS
Instance Method Summary collapse
- #add_viewer_options(opts) ⇒ Object
- #any_filter_active? ⇒ Boolean
- #build_parser ⇒ Object
-
#deliver(payload, binary:, edges:, status: silent_status) ⇒ Object
Writes the payload to the configured destination and opens the browser when the html-default flow applies.
-
#effective_collapse(edges) ⇒ Object
Choose collapse prefixes.
-
#effective_output_path ⇒ Object
Resolve the output path.
-
#graphviz_svg(dot_source) ⇒ Object
Shell out to Graphviz ‘dot -Tsvg`.
- #html? ⇒ Boolean
-
#initialize(stdout:, stderr:) ⇒ View
constructor
A new instance of View.
- #open_in_browser(path) ⇒ Object
- #package_groups(edges) ⇒ Object
-
#render_payload(edges, nodes, collapse, groups) ⇒ Object
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).
- #render_subtitle(edges, collapse, groups) ⇒ Object
-
#restrict_nodes_to_edges(nodes, edges) ⇒ Object
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.
- #rigor_step_label(paths) ⇒ Object
- #run(argv) ⇒ Object
- #silent_status ⇒ Object
Methods included from EdgeFilters
#add_filter_options, #apply_filters, #validate!
Constructor Details
#initialize(stdout:, stderr:) ⇒ View
Returns a new instance of View.
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 |
# 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 |
Instance Method Details
#add_viewer_options(opts) ⇒ Object
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 |
# File 'lib/rigor/module_graph/cli.rb', line 655 def (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 |
#any_filter_active? ⇒ Boolean
464 465 466 467 |
# File 'lib/rigor/module_graph/cli.rb', line 464 def any_filter_active? @options[:kinds] || @options[:confidences] || @options[:from] || @options[:depth] end |
#build_parser ⇒ Object
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 |
# File 'lib/rigor/module_graph/cli.rb', line 587 def build_parser OptionParser.new do |opts| opts. = "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 (opts) (opts, @options) opts.on("-h", "--help") do @stdout.puts opts exit 0 end end end |
#deliver(payload, binary:, edges:, status: silent_status) ⇒ Object
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.
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 |
# 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 |
#effective_collapse(edges) ⇒ Object
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.
675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 |
# 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 |
#effective_output_path ⇒ Object
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.
576 577 578 579 580 581 |
# 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 |
#graphviz_svg(dot_source) ⇒ Object
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.
534 535 536 537 538 539 540 541 542 543 544 |
# 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 |
#html? ⇒ Boolean
583 584 585 |
# File 'lib/rigor/module_graph/cli.rb', line 583 def html? %w[html mermaid-html].include?(@options[:format]) end |
#open_in_browser(path) ⇒ Object
737 738 739 740 741 742 743 |
# 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.}" end |
#package_groups(edges) ⇒ Object
724 725 726 727 728 729 730 731 732 733 734 735 |
# File 'lib/rigor/module_graph/cli.rb', line 724 def package_groups(edges) return nil unless @options[:package] = PackwerkOverlay.discover(@options[:package]) unless .any? @stderr.puts "rigor-module-graph view: no package.yml found under " \ "#{@options[:package].inspect}; falling back to namespace collapse" return nil end .groups_for(edges) end |
#render_payload(edges, nodes, collapse, groups) ⇒ Object
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).
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 |
# 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 |
#render_subtitle(edges, collapse, groups) ⇒ Object
699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 |
# 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 |
#restrict_nodes_to_edges(nodes, edges) ⇒ Object
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).
523 524 525 526 527 528 529 |
# 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 |
#rigor_step_label(paths) ⇒ Object
459 460 461 462 |
# 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 |
#run(argv) ⇒ Object
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 |
# 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.}" 2 rescue CollectError, RenderError => e @stderr.puts "rigor-module-graph view: #{e.}" 1 end |
#silent_status ⇒ Object
469 470 471 |
# File 'lib/rigor/module_graph/cli.rb', line 469 def silent_status Rigor::ModuleGraph::StatusReporter.new(stderr: @stderr, quiet: true) end |