rigortype 0.1.7 → 0.1.8

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: 0eaff9cf0ef65d44ceb3666a23fb77003a3dbb0361d890e1d2991ef6539499de
4
- data.tar.gz: e7fdc58be21409504965f35479559d26bcf4726ba0feabe3fd5128bcffe8419b
3
+ metadata.gz: 9880f534e47f36db5fbbaa5e6d169350f90ecf1ed12081e4ccf5e5e01e9d4518
4
+ data.tar.gz: 1abb8764932adadaafc6cf43152d2306002aa04419ec403e81e63eed3cedb08e
5
5
  SHA512:
6
- metadata.gz: 94aae7605ca3243e7226e6f2e1c844f141d3ef04995751718e08ef5fb9dfa550455c6c87420e731332b765ee262442ed2608b5f0d7b05a25b982615b993114e5
7
- data.tar.gz: f2dedba8fb33b9f7d98ddaa4debcec042edf56396c22a791ce8897736839c559240cb20158916f7c2bc5f483da06c1bebb7212ec2f24b16c3554164f849da621
6
+ metadata.gz: c9135e0d12d588614b7fc1c4af793aab3b798f42bae19e0a72d15ba875e979f24927abcce8976431d7694b856bdc89fc94c588874414fdeee5f437c915af956d
7
+ data.tar.gz: d77dbf70aa13cb3418636e31ff498384ebefd21e0c6887324cc316975e862d1a09c86dc06f313fa081b17fd5f0f7c1592ee786f1958be66ab02e697c5630b80d
@@ -1215,7 +1215,9 @@ module Rigor
1215
1215
  line: location.start_line,
1216
1216
  column: location.start_column + 1,
1217
1217
  message: "undefined method `#{call_node.name}' for #{rendered_receiver}",
1218
- severity: :error
1218
+ severity: :error,
1219
+ receiver_type: rendered_receiver,
1220
+ method_name: call_node.name.to_s
1219
1221
  )
1220
1222
  end
1221
1223
 
@@ -8,7 +8,8 @@ module Rigor
8
8
  # baseline against which non-default families are recognised.
9
9
  DEFAULT_SOURCE_FAMILY = :builtin
10
10
 
11
- attr_reader :path, :line, :column, :message, :severity, :rule, :source_family
11
+ attr_reader :path, :line, :column, :message, :severity, :rule, :source_family,
12
+ :receiver_type, :method_name
12
13
 
13
14
  # `rule:` is the stable identifier (a kebab-case string)
14
15
  # of the diagnostic's source rule. It is used by the
@@ -24,8 +25,19 @@ module Rigor
24
25
  # ADR-2 § "Plugin Diagnostic Provenance") let consumers
25
26
  # distinguish where a diagnostic originated without committing
26
27
  # to the plugin API itself.
27
- def initialize(path:, line:, column:, message:, severity: :error, rule: nil,
28
- source_family: DEFAULT_SOURCE_FAMILY)
28
+ #
29
+ # `receiver_type:` / `method_name:` are optional structured
30
+ # fields populated by the call-related rules (`call.undefined-
31
+ # method`) — the rendered receiver type and the called method
32
+ # name as plain strings. ADR-23 WD3 / slice 4: `rigor triage`'s
33
+ # heuristic recognisers read these directly instead of parsing
34
+ # the diagnostic message, so the catalogue no longer couples to
35
+ # message wording. Both stay nil for rules that have no such
36
+ # pair; a consumer that finds them nil falls back to message
37
+ # parsing.
38
+ def initialize(path:, line:, column:, message:, severity: :error, rule: nil, # rubocop:disable Metrics/ParameterLists
39
+ source_family: DEFAULT_SOURCE_FAMILY,
40
+ receiver_type: nil, method_name: nil)
29
41
  @path = path
30
42
  @line = line
31
43
  @column = column
@@ -33,6 +45,8 @@ module Rigor
33
45
  @severity = severity
34
46
  @rule = rule
35
47
  @source_family = source_family
48
+ @receiver_type = receiver_type
49
+ @method_name = method_name
36
50
  end
37
51
 
38
52
  def error?
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
+ require "tmpdir"
4
5
 
5
6
  require_relative "../environment"
6
7
  require_relative "../scope"
@@ -104,6 +105,9 @@ module Rigor
104
105
  @signature_paths_snapshot = [].freeze
105
106
  @cached_plugin_prepare_diagnostics = [].freeze
106
107
  @project_discovered_classes = {}.freeze
108
+ @project_discovered_def_nodes = {}.freeze
109
+ @project_discovered_superclasses = {}.freeze
110
+ @project_discovered_includes = {}.freeze
107
111
  end
108
112
 
109
113
  # ADR-pending editor mode — present when the runner is wired
@@ -241,6 +245,16 @@ module Rigor
241
245
  # can share parses with the existing scanner passes.
242
246
  @project_discovered_classes =
243
247
  Inference::ScopeIndexer.discovered_classes_for_paths(expansion.fetch(:files), buffer: @buffer)
248
+ # ADR-24 slice 2 — cross-file def-node + class->superclass
249
+ # index so an implicit-self call inside a subclass
250
+ # resolves a superclass `def` declared in a sibling
251
+ # file. One extra parse pass over the project; shares
252
+ # the cost profile of the class-discovery pass above.
253
+ def_index =
254
+ Inference::ScopeIndexer.discovered_def_index_for_paths(expansion.fetch(:files), buffer: @buffer)
255
+ @project_discovered_def_nodes = def_index.fetch(:def_nodes)
256
+ @project_discovered_superclasses = def_index.fetch(:superclasses)
257
+ @project_discovered_includes = def_index.fetch(:includes)
244
258
  end
245
259
 
246
260
  # Internal: adopts a frozen {ProjectScan} snapshot supplied
@@ -278,7 +292,7 @@ module Rigor
278
292
  return [] if files.empty?
279
293
 
280
294
  if pool_mode?
281
- analyze_files_in_pool(files)
295
+ dispatch_pool(files)
282
296
  else
283
297
  environment = resolve_sequential_environment
284
298
  result = files.flat_map { |path| analyze_file(path, environment) }
@@ -460,6 +474,34 @@ module Rigor
460
474
  @buffer.nil?
461
475
  end
462
476
 
477
+ # ADR-15 Amendment (2026-05-20) — worker-pool backend selector.
478
+ # `fork` is the active backend: separate processes sidestep both
479
+ # the Ruby Bug #22075 use-after-free and the worker-side
480
+ # `Ractor::IsolationError` that make the Ractor pool unusable
481
+ # (see the ADR-15 Amendment +
482
+ # docs/notes/20260520-ractor-pool-cruby-uaf.md). The Ractor pool
483
+ # is preserved but off the default path — `RIGOR_POOL_BACKEND=ractor`
484
+ # opts back in so it stays testable. Platforms without `fork`
485
+ # (Windows) fall back to sequential.
486
+ def pool_backend
487
+ return :ractor if ENV["RIGOR_POOL_BACKEND"] == "ractor"
488
+ return :fork if Process.respond_to?(:fork)
489
+
490
+ :sequential
491
+ end
492
+
493
+ # Routes pool-mode analysis to the selected backend.
494
+ def dispatch_pool(files)
495
+ case pool_backend
496
+ when :ractor then analyze_files_in_pool(files)
497
+ when :fork then analyze_files_in_fork_pool(files)
498
+ else
499
+ analyze_files_sequentially_fallback(
500
+ files, reason: "fork-based parallelism is unavailable on this platform"
501
+ )
502
+ end
503
+ end
504
+
463
505
  # Coordinator-side Environment used by the sequential code
464
506
  # path. Pool mode builds one Environment per worker inside
465
507
  # the worker Ractor's body instead.
@@ -598,6 +640,122 @@ module Rigor
598
640
  Array(prepare_diagnostics) + files.flat_map { |path| results_by_path.fetch(path, []) }
599
641
  end
600
642
 
643
+ # ADR-15 Amendment (2026-05-20) — fork-based worker pool, the
644
+ # active backend for `workers > 0`. Builds ONE {WorkerSession}
645
+ # on the parent, then `fork`s N children that copy-on-write
646
+ # inherit it. Each child analyses a contiguous slice of `files`
647
+ # and writes a Marshal'd `{results:, reporters:}` payload to a
648
+ # temp file; the parent `Process.wait`s every child, merges the
649
+ # payloads, and re-orders diagnostics by original path order.
650
+ #
651
+ # Separate processes have separate GC heaps and `vm->ci_table`
652
+ # (immune to Ruby Bug #22075) and copy-on-write-inherit every
653
+ # constant (no `Ractor.shareable?` constraint). See the ADR-15
654
+ # Amendment + docs/notes/20260520-ractor-pool-cruby-uaf.md.
655
+ #
656
+ # A child that exits non-zero (crash / unmarshalable payload) is
657
+ # degraded: the parent re-analyses that slice in-process and
658
+ # prepends a `pool-degraded` warning.
659
+ def analyze_files_in_fork_pool(files) # rubocop:disable Metrics/AbcSize
660
+ Environment::ClassRegistry.default
661
+
662
+ session = WorkerSession.new(
663
+ configuration: @configuration,
664
+ cache_store: @cache_store,
665
+ plugin_blueprints: @plugin_registry.blueprints,
666
+ explain: @explain,
667
+ synthetic_method_index: @synthetic_method_index,
668
+ project_patched_methods: @project_patched_methods
669
+ )
670
+ # Force the full RBS load on the parent so children
671
+ # copy-on-write inherit a warm Environment rather than each
672
+ # rebuilding it after the fork.
673
+ session.environment.rbs_loader&.prewarm
674
+ snapshot_fork_pool_stats(session) if @collect_stats
675
+
676
+ worker_count = [@workers, files.size].min
677
+ slices = files.each_slice((files.size.to_f / worker_count).ceil).to_a
678
+ results_by_path = {}
679
+
680
+ degraded = Dir.mktmpdir("rigor-fork-pool") do |tmpdir|
681
+ children = slices.each_with_index.map do |slice, index|
682
+ out_path = File.join(tmpdir, "worker-#{index}")
683
+ { pid: fork { run_fork_worker(session, slice, out_path) },
684
+ slice: slice, out_path: out_path }
685
+ end
686
+ collect_fork_results(children, results_by_path)
687
+ end
688
+
689
+ unless degraded.empty?
690
+ degraded.each { |path| results_by_path[path] = session.analyze(path) }
691
+ merge_worker_reporters(session.drain_reporters)
692
+ end
693
+
694
+ diagnostics = Array(session.prepare_diagnostics) +
695
+ files.flat_map { |path| results_by_path.fetch(path, []) }
696
+ degraded.empty? ? diagnostics : diagnostics.unshift(fork_degraded_diagnostic(degraded.size))
697
+ end
698
+
699
+ # Child-process body for {#analyze_files_in_fork_pool}. Analyses
700
+ # the slice with the copy-on-write-inherited session and writes
701
+ # the Marshal'd payload to `out_path`. `exit!` skips `at_exit` /
702
+ # stdio flush — the payload is already durable on disk by then.
703
+ def run_fork_worker(session, slice, out_path)
704
+ results = slice.to_h { |path| [path, session.analyze(path)] }
705
+ payload = { results: results, reporters: session.drain_reporters }
706
+ File.binwrite(out_path, Marshal.dump(payload))
707
+ exit!(0)
708
+ rescue StandardError
709
+ exit!(1)
710
+ end
711
+
712
+ # Snapshots `class_decl_paths` from the parent session's loader
713
+ # so end-of-run {RunStats} can attribute the RBS class universe.
714
+ def snapshot_fork_pool_stats(session)
715
+ loader = session.environment.rbs_loader
716
+ @class_decl_paths_snapshot = loader&.class_decl_paths || {}.freeze
717
+ @signature_paths_snapshot = loader&.signature_paths || [].freeze
718
+ end
719
+
720
+ # Waits for every forked child, merges each successful payload
721
+ # into `results_by_path`, and returns the file paths whose
722
+ # worker exited abnormally (for in-process degrade).
723
+ def collect_fork_results(children, results_by_path)
724
+ degraded = []
725
+ children.each do |child|
726
+ _, status = Process.waitpid2(child[:pid])
727
+ payload = fork_worker_payload(status, child[:out_path])
728
+ if payload
729
+ results_by_path.merge!(payload.fetch(:results))
730
+ merge_worker_reporters(payload.fetch(:reporters))
731
+ else
732
+ degraded.concat(child[:slice])
733
+ end
734
+ end
735
+ degraded
736
+ end
737
+
738
+ # @return [Hash, nil] the child's `{results:, reporters:}`
739
+ # payload, or nil when the child exited abnormally or wrote no
740
+ # readable payload. `Marshal.load` is safe here: the blob was
741
+ # written by our own forked child to a temp file we created.
742
+ def fork_worker_payload(status, out_path)
743
+ return nil unless status.success? && File.exist?(out_path)
744
+
745
+ Marshal.load(File.binread(out_path)) # rubocop:disable Security/MarshalLoad
746
+ rescue StandardError
747
+ nil
748
+ end
749
+
750
+ def fork_degraded_diagnostic(count)
751
+ Diagnostic.new(
752
+ path: ".rigor.yml", line: 1, column: 1,
753
+ message: "fork pool degraded: #{count} file(s) re-analysed in-process " \
754
+ "after a worker exited abnormally",
755
+ severity: :warning, rule: "pool-degraded", source_family: :builtin
756
+ )
757
+ end
758
+
601
759
  # End-of-run telemetry. Walks the cached
602
760
  # `class_decl_paths` snapshot (sequential mode: from
603
761
  # the coordinator's environment; pool mode: from the
@@ -1221,12 +1379,29 @@ module Rigor
1221
1379
  Prism.parse(File.read(physical), filepath: path, version: @configuration.target_ruby)
1222
1380
  end
1223
1381
 
1382
+ # Seeds the cross-file project pre-pass indexes onto a
1383
+ # fresh per-file scope: discovered classes, and the ADR-24
1384
+ # def-node / superclass / included-module maps. Each is
1385
+ # applied only when non-empty so a runner constructed
1386
+ # without the project pre-pass (e.g. a single-file probe)
1387
+ # keeps an empty seed.
1388
+ def seed_project_scope(scope)
1389
+ scope = scope.with_discovered_classes(@project_discovered_classes) unless @project_discovered_classes.empty?
1390
+ unless @project_discovered_def_nodes.empty?
1391
+ scope = scope.with_discovered_def_nodes(@project_discovered_def_nodes)
1392
+ end
1393
+ unless @project_discovered_superclasses.empty?
1394
+ scope = scope.with_discovered_superclasses(@project_discovered_superclasses)
1395
+ end
1396
+ scope = scope.with_discovered_includes(@project_discovered_includes) unless @project_discovered_includes.empty?
1397
+ scope
1398
+ end
1399
+
1224
1400
  def analyze_file(path, environment) # rubocop:disable Metrics/MethodLength
1225
1401
  parse_result = parse_source(path)
1226
1402
  return parse_diagnostics(path, parse_result) unless parse_result.errors.empty?
1227
1403
 
1228
- scope = Scope.empty(environment: environment, source_path: path)
1229
- scope = scope.with_discovered_classes(@project_discovered_classes) unless @project_discovered_classes.empty?
1404
+ scope = seed_project_scope(Scope.empty(environment: environment, source_path: path))
1230
1405
  index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
1231
1406
  diagnostics = CheckRules.diagnose(
1232
1407
  path: path,
@@ -38,6 +38,12 @@ module Rigor
38
38
  # - `plugin_blueprints` — Phase 3a
39
39
  # (`Array<Plugin::Blueprint>` is `Ractor.shareable?`).
40
40
  # - `explain` — Boolean.
41
+ # - `synthetic_method_index` / `project_patched_methods` —
42
+ # optional (default `nil`). NOT `Ractor.shareable?`, so the
43
+ # Ractor pool path leaves them unset; the fork backend
44
+ # (ADR-15 Amendment), which builds the session pre-fork on the
45
+ # parent, threads the runner's project-scan results through so
46
+ # per-file inference matches the sequential path exactly.
41
47
  #
42
48
  # Internally the session OWNS (and never shares):
43
49
  #
@@ -70,7 +76,7 @@ module Rigor
70
76
  # output (modulo severity-profile re-stamping, which the
71
77
  # session leaves to the caller because it is a per-run
72
78
  # aggregate concern).
73
- class WorkerSession
79
+ class WorkerSession # rubocop:disable Metrics/ClassLength
74
80
  attr_reader :configuration, :cache_store, :services, :plugin_registry,
75
81
  :dependency_source_index, :environment,
76
82
  :rbs_extended_reporter, :boundary_cross_reporter,
@@ -88,11 +94,14 @@ module Rigor
88
94
  # directly-unrecognised node, mirroring
89
95
  # {Rigor::Analysis::Runner#explain_diagnostics}.
90
96
  def initialize(configuration:, cache_store: nil, # rubocop:disable Metrics/MethodLength
91
- plugin_blueprints: [], explain: false, buffer: nil)
97
+ plugin_blueprints: [], explain: false, buffer: nil,
98
+ synthetic_method_index: nil, project_patched_methods: nil)
92
99
  @configuration = configuration
93
100
  @cache_store = cache_store
94
101
  @explain = explain
95
102
  @buffer = buffer
103
+ @synthetic_method_index = synthetic_method_index
104
+ @project_patched_methods = project_patched_methods
96
105
 
97
106
  # NOTE: `Inference::MethodDispatcher::FileFolding.fold_platform_specific_paths`
98
107
  # is process-global state. Writing it from a non-main
@@ -127,7 +136,9 @@ module Rigor
127
136
  bundler_auto_detect: configuration.bundler_auto_detect,
128
137
  bundler_lockfile: configuration.bundler_lockfile,
129
138
  rbs_collection_lockfile: configuration.rbs_collection_lockfile,
130
- rbs_collection_auto_detect: configuration.rbs_collection_auto_detect
139
+ rbs_collection_auto_detect: configuration.rbs_collection_auto_detect,
140
+ synthetic_method_index: @synthetic_method_index,
141
+ project_patched_methods: @project_patched_methods
131
142
  )
132
143
  @prepare_diagnostics = run_plugin_prepare.freeze
133
144
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optionparser"
4
+
5
+ require_relative "../configuration"
6
+ require_relative "../analysis/runner"
7
+ require_relative "../cache/store"
8
+ require_relative "../triage"
9
+ require_relative "triage_renderer"
10
+
11
+ module Rigor
12
+ class CLI
13
+ # ADR-23 — executes `rigor triage`.
14
+ #
15
+ # Runs the same analysis as `rigor check`, then summarises the
16
+ # diagnostic stream (rule distribution, per-file hotspots,
17
+ # heuristic hints) instead of printing the raw per-line list.
18
+ # Read-only and advisory (WD4): never edits config, never
19
+ # writes a baseline. Always exits 0 — it is an inspection
20
+ # command, not a gate (`rigor check` remains the gate).
21
+ class TriageCommand
22
+ USAGE = "Usage: rigor triage [options] [paths]"
23
+ DEFAULT_SECTIONS = %i[distribution hotspots hints].freeze
24
+
25
+ def initialize(argv:, out:, err:)
26
+ @argv = argv
27
+ @out = out
28
+ @err = err
29
+ end
30
+
31
+ # @return [Integer] CLI exit status (always 0).
32
+ def run
33
+ options = parse_options
34
+ configuration = Configuration.load(options.fetch(:config))
35
+ diagnostics = analyze(configuration)
36
+
37
+ report = Triage.analyze(diagnostics, top: options.fetch(:top),
38
+ hints: options.fetch(:sections).include?(:hints))
39
+ renderer = TriageRenderer.new(report, sections: options.fetch(:sections))
40
+ @out.puts(options.fetch(:format) == "json" ? renderer.json : renderer.text)
41
+ 0
42
+ end
43
+
44
+ private
45
+
46
+ def parse_options
47
+ options = { config: nil, format: "text", top: 10, sections: DEFAULT_SECTIONS }
48
+ OptionParser.new do |opts|
49
+ opts.banner = USAGE
50
+ opts.on("--config=PATH", "Path to the Rigor configuration file") { |v| options[:config] = v }
51
+ opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
52
+ opts.on("--top=N", Integer, "Hotspot-file count (default 10)") { |v| options[:top] = v }
53
+ opts.on("--hints-only", "Print only the heuristic-hints section") { options[:sections] = %i[hints] }
54
+ opts.on("--no-hints", "Print distribution + hotspots only") do
55
+ options[:sections] = %i[distribution hotspots]
56
+ end
57
+ end.parse!(@argv)
58
+ validate!(options)
59
+ options
60
+ end
61
+
62
+ def validate!(options)
63
+ return if %w[text json].include?(options.fetch(:format))
64
+
65
+ raise OptionParser::InvalidArgument, "unsupported format: #{options.fetch(:format)}"
66
+ end
67
+
68
+ # Sequential, cache-backed, no run-stats: triage only needs
69
+ # the diagnostic stream, and sequential keeps the rule
70
+ # distribution deterministic (the fork pool's cross-file
71
+ # divergence would skew the histogram).
72
+ def analyze(configuration)
73
+ runner = Analysis::Runner.new(
74
+ configuration: configuration,
75
+ cache_store: Cache::Store.new(root: configuration.cache_path),
76
+ collect_stats: false,
77
+ workers: 0
78
+ )
79
+ runner.run(@argv.empty? ? configuration.paths : @argv).diagnostics
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "../triage"
6
+
7
+ module Rigor
8
+ class CLI
9
+ # ADR-23 — renders a {Rigor::Triage::Report} as the `rigor
10
+ # triage` text report or as `--format json`.
11
+ class TriageRenderer
12
+ BAR_WIDTH = 24
13
+
14
+ def initialize(report, sections:)
15
+ @report = report
16
+ @sections = sections # subset of %i[distribution hotspots hints]
17
+ end
18
+
19
+ def json
20
+ JSON.pretty_generate(Triage.report_to_h(@report))
21
+ end
22
+
23
+ def text
24
+ blocks = []
25
+ blocks << distribution_block if @sections.include?(:distribution)
26
+ blocks << hotspots_block if @sections.include?(:hotspots)
27
+ blocks << hints_block if @sections.include?(:hints)
28
+ "#{blocks.join("\n\n")}\n"
29
+ end
30
+
31
+ private
32
+
33
+ def distribution_block
34
+ s = @report.summary
35
+ max = @report.distribution.map(&:count).max || 1
36
+ lines = ["Diagnostic distribution — #{s.total} total " \
37
+ "(#{s.error} error / #{s.warning} warning#{" / #{s.info} info" if s.info.positive?})"]
38
+ @report.distribution.each do |row|
39
+ lines << format(" %<rule>-32s %<count>5d %<bar>s",
40
+ rule: row.rule, count: row.count, bar: bar(row.count, max))
41
+ end
42
+ lines.join("\n")
43
+ end
44
+
45
+ def hotspots_block
46
+ return "Hotspot files\n (none)" if @report.hotspots.empty?
47
+
48
+ lines = ["Hotspot files"]
49
+ @report.hotspots.each do |spot|
50
+ by_rule = spot.by_rule.map { |rule, count| "#{rule}×#{count}" }.join(" ")
51
+ lines << format(" %<file>-40s %<count>4d %<rules>s",
52
+ file: spot.file, count: spot.count, rules: by_rule)
53
+ end
54
+ lines.join("\n")
55
+ end
56
+
57
+ def hints_block
58
+ return "Hints\n (no heuristic hints)" if @report.hints.empty?
59
+
60
+ lines = ["Hints — heuristics, verify before acting"]
61
+ @report.hints.each do |hint|
62
+ lines << ""
63
+ lines << " [#{hint.confidence} #{hint.id}] #{hint.diagnostic_count} diagnostic(s)"
64
+ lines << " #{hint.summary}"
65
+ lines << " → #{hint.action}"
66
+ end
67
+ lines.join("\n")
68
+ end
69
+
70
+ def bar(count, max)
71
+ filled = max.zero? ? 0 : (count * BAR_WIDTH / max)
72
+ filled = 1 if filled.zero? && count.positive?
73
+ "█" * filled
74
+ end
75
+ end
76
+ end
77
+ end
data/lib/rigor/cli.rb CHANGED
@@ -27,7 +27,8 @@ module Rigor
27
27
  "diff" => :run_diff,
28
28
  "sig-gen" => :run_sig_gen,
29
29
  "lsp" => :run_lsp,
30
- "baseline" => :run_baseline
30
+ "baseline" => :run_baseline,
31
+ "triage" => :run_triage
31
32
  }.freeze
32
33
 
33
34
  def self.start(argv = ARGV, out: $stdout, err: $stderr)
@@ -464,6 +465,12 @@ module Rigor
464
465
  BaselineCommand.new(argv: @argv, out: @out, err: @err).run
465
466
  end
466
467
 
468
+ def run_triage
469
+ require_relative "cli/triage_command"
470
+
471
+ CLI::TriageCommand.new(argv: @argv, out: @out, err: @err).run
472
+ end
473
+
467
474
  def write_result(result, format)
468
475
  case format
469
476
  when "json"
@@ -505,6 +512,7 @@ module Rigor
505
512
  diff Compare current diagnostics to a saved baseline JSON
506
513
  sig-gen Emit RBS skeletons inferred from .rb sources (ADR-14)
507
514
  lsp Run the Rigor Language Server (LSP) over stdio
515
+ triage Summarise diagnostics: distribution, hotspots, hints (ADR-23)
508
516
  version Print the Rigor version
509
517
  help Print this help
510
518
  HELP