Class: Rigor::ModuleGraph::CLI::StatsCmd

Inherits:
Object
  • Object
show all
Includes:
EdgeFilters
Defined in:
lib/rigor/module_graph/cli.rb

Overview

‘stats` reports the fan-out / fan-in / internal / nodes numbers per namespace. Same filter flags as the renderers so a focused subgraph can be summarised without regenerating the JSONL.

Constant Summary collapse

FORMATS =
%w[text json].freeze
HEADERS =
%w[namespace nodes fan-out fan-in internal total].freeze

Constants included from EdgeFilters

EdgeFilters::VALID_CONFIDENCES, EdgeFilters::VALID_DIRECTIONS, EdgeFilters::VALID_EDGE_SCOPES, EdgeFilters::VALID_KINDS

Instance Method Summary collapse

Methods included from EdgeFilters

#add_filter_options, #apply_filters, #validate!

Constructor Details

#initialize(stdout:, stderr:, stdin:) ⇒ StatsCmd

Returns a new instance of StatsCmd.



956
957
958
959
960
961
962
963
964
965
# File 'lib/rigor/module_graph/cli.rb', line 956

def initialize(stdout:, stderr:, stdin:)
  @stdout = stdout
  @stderr = stderr
  @stdin = stdin
  @state = {
    kinds: nil, confidences: nil,
    from: nil, depth: nil, direction: :both, edge_scope: :cluster,
    grouping_depth: 1, format: "text", limit: nil
  }
end

Instance Method Details

#format_row(row, widths) ⇒ Object



1049
1050
1051
1052
1053
# File 'lib/rigor/module_graph/cli.rb', line 1049

def format_row(row, widths)
  row.each_with_index.map do |cell, idx|
    idx.zero? ? cell.ljust(widths[idx]) : cell.rjust(widths[idx])
  end.join("  ")
end

#format_table(metrics) ⇒ Object

A space-padded text table sized to the widest cell per column. Numeric columns are right-aligned so a quick eye-scan finds the hotspots.



1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
# File 'lib/rigor/module_graph/cli.rb', line 1031

def format_table(metrics)
  if metrics.empty?
    return "(no edges)\n"
  end

  rows = metrics.map do |m|
    [m.namespace, m.nodes.to_s, m.fan_out.to_s, m.fan_in.to_s,
     m.internal.to_s, m.total.to_s]
  end
  widths = HEADERS.zip(*rows).map { |col| col.map(&:length).max }

  out = +""
  out << format_row(HEADERS, widths) << "\n"
  out << ("-" * widths.sum { |w| w + 2 }) << "\n"
  rows.each { |row| out << format_row(row, widths) << "\n" }
  out
end

#parse_options!(argv) ⇒ Object



995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
# File 'lib/rigor/module_graph/cli.rb', line 995

def parse_options!(argv)
  parser = OptionParser.new do |opts|
    opts.banner = "Usage: rigor-module-graph stats [options] [FILE]"
    opts.on("--grouping-depth N", Integer,
            "How many leading namespace segments to group by (default: 1)") do |n|
      @state[:grouping_depth] = n
    end
    opts.on("--limit N", Integer,
            "Show only the top N namespaces by fan-out") do |n|
      @state[:limit] = n
    end
    opts.on("--format FORMAT", FORMATS,
            "Output format (#{FORMATS.join("/")}; default: text)") do |fmt|
      @state[:format] = fmt
    end
    add_filter_options(opts, @state)
    opts.on("-h", "--help") do
      @stdout.puts opts
      exit 0
    end
  end
  parser.parse!(argv)
end

#render(metrics) ⇒ Object



1019
1020
1021
1022
1023
1024
1025
1026
# File 'lib/rigor/module_graph/cli.rb', line 1019

def render(metrics)
  case @state[:format]
  when "json"
    @stdout.puts(JSON.pretty_generate(metrics.map(&:to_h)))
  when "text"
    @stdout.print(format_table(metrics))
  end
end

#run(argv) ⇒ Object



967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
# File 'lib/rigor/module_graph/cli.rb', line 967

def run(argv)
  argv = argv.dup
  parse_options!(argv)
  path, = argv
  io = path ? File.open(path, "r") : @stdin
  begin
    edges = EdgeIO.read(io)
  ensure
    io.close if path && !io.closed?
  end
  edges = apply_filters(
    edges,
    kinds: @state[:kinds],
    confidences: @state[:confidences],
    from: @state[:from],
    depth: @state[:depth],
    direction: @state[:direction],
    edge_scope: @state[:edge_scope]
  )
  metrics = Stats.compute(edges, depth: @state[:grouping_depth])
  metrics = metrics.first(@state[:limit]) if @state[:limit]
  render(metrics)
  0
rescue OptionParser::ParseError => e
  @stderr.puts "rigor-module-graph stats: #{e.message}"
  2
end