rigortype 0.1.3 → 0.1.5

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.
Files changed (149) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +154 -33
  3. data/lib/rigor/analysis/check_rules.rb +10 -18
  4. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
  5. data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
  9. data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
  10. data/lib/rigor/analysis/diagnostic.rb +0 -2
  11. data/lib/rigor/analysis/fact_store.rb +26 -6
  12. data/lib/rigor/analysis/result.rb +11 -3
  13. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  14. data/lib/rigor/analysis/run_stats.rb +193 -0
  15. data/lib/rigor/analysis/runner.rb +498 -12
  16. data/lib/rigor/analysis/worker_session.rb +327 -0
  17. data/lib/rigor/builtins/imported_refinements.rb +364 -55
  18. data/lib/rigor/builtins/regex_refinement.rb +17 -12
  19. data/lib/rigor/cache/descriptor.rb +1 -1
  20. data/lib/rigor/cache/rbs_descriptor.rb +3 -1
  21. data/lib/rigor/cache/store.rb +39 -6
  22. data/lib/rigor/cli/diff_command.rb +1 -1
  23. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  24. data/lib/rigor/cli/type_of_command.rb +1 -1
  25. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  26. data/lib/rigor/cli/type_scan_report.rb +2 -2
  27. data/lib/rigor/cli.rb +61 -3
  28. data/lib/rigor/configuration/dependencies.rb +2 -2
  29. data/lib/rigor/configuration.rb +131 -6
  30. data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
  31. data/lib/rigor/environment/class_registry.rb +12 -3
  32. data/lib/rigor/environment/lockfile_resolver.rb +125 -0
  33. data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
  34. data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
  35. data/lib/rigor/environment/rbs_loader.rb +194 -6
  36. data/lib/rigor/environment/reflection.rb +152 -0
  37. data/lib/rigor/environment.rb +109 -6
  38. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  39. data/lib/rigor/flow_contribution/element.rb +1 -1
  40. data/lib/rigor/flow_contribution/fact.rb +1 -1
  41. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  42. data/lib/rigor/flow_contribution/merger.rb +3 -3
  43. data/lib/rigor/flow_contribution.rb +2 -2
  44. data/lib/rigor/inference/acceptance.rb +35 -1
  45. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  46. data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
  47. data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
  48. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  49. data/lib/rigor/inference/expression_typer.rb +77 -11
  50. data/lib/rigor/inference/fallback.rb +1 -1
  51. data/lib/rigor/inference/macro_block_self_type.rb +96 -0
  52. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  53. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -41
  54. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  55. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
  56. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  57. data/lib/rigor/inference/method_dispatcher/method_folding.rb +135 -0
  58. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +7 -12
  59. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  60. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -44
  61. data/lib/rigor/inference/method_dispatcher.rb +274 -5
  62. data/lib/rigor/inference/method_parameter_binder.rb +22 -14
  63. data/lib/rigor/inference/narrowing.rb +129 -12
  64. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  65. data/lib/rigor/inference/scope_indexer.rb +14 -9
  66. data/lib/rigor/inference/statement_evaluator.rb +7 -7
  67. data/lib/rigor/inference/synthetic_method.rb +86 -0
  68. data/lib/rigor/inference/synthetic_method_index.rb +82 -0
  69. data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
  70. data/lib/rigor/plugin/blueprint.rb +60 -0
  71. data/lib/rigor/plugin/io_boundary.rb +0 -2
  72. data/lib/rigor/plugin/loader.rb +5 -3
  73. data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
  74. data/lib/rigor/plugin/macro/external_file.rb +143 -0
  75. data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
  76. data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
  77. data/lib/rigor/plugin/macro.rb +31 -0
  78. data/lib/rigor/plugin/manifest.rb +102 -10
  79. data/lib/rigor/plugin/registry.rb +43 -2
  80. data/lib/rigor/plugin/services.rb +1 -1
  81. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  82. data/lib/rigor/plugin.rb +2 -0
  83. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  84. data/lib/rigor/rbs_extended.rb +131 -32
  85. data/lib/rigor/scope.rb +25 -8
  86. data/lib/rigor/sig_gen/classification.rb +36 -0
  87. data/lib/rigor/sig_gen/generator.rb +1048 -0
  88. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  89. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  90. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  91. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  92. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  93. data/lib/rigor/sig_gen/renderer.rb +157 -0
  94. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  95. data/lib/rigor/sig_gen/write_result.rb +48 -0
  96. data/lib/rigor/sig_gen/writer.rb +530 -0
  97. data/lib/rigor/sig_gen.rb +25 -0
  98. data/lib/rigor/trinary.rb +15 -11
  99. data/lib/rigor/type/bot.rb +6 -3
  100. data/lib/rigor/type/bound_method.rb +79 -0
  101. data/lib/rigor/type/combinator.rb +207 -3
  102. data/lib/rigor/type/constant.rb +13 -0
  103. data/lib/rigor/type/hash_shape.rb +0 -2
  104. data/lib/rigor/type/integer_range.rb +7 -7
  105. data/lib/rigor/type/refined.rb +18 -12
  106. data/lib/rigor/type/top.rb +4 -3
  107. data/lib/rigor/type/union.rb +20 -1
  108. data/lib/rigor/type.rb +1 -0
  109. data/lib/rigor/type_node/generic.rb +68 -0
  110. data/lib/rigor/type_node/identifier.rb +38 -0
  111. data/lib/rigor/type_node/indexed_access.rb +41 -0
  112. data/lib/rigor/type_node/integer_literal.rb +29 -0
  113. data/lib/rigor/type_node/name_scope.rb +52 -0
  114. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  115. data/lib/rigor/type_node/string_literal.rb +32 -0
  116. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  117. data/lib/rigor/type_node/union.rb +42 -0
  118. data/lib/rigor/type_node.rb +29 -0
  119. data/lib/rigor/version.rb +1 -1
  120. data/lib/rigor.rb +2 -0
  121. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  122. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  123. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  124. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  125. data/sig/rigor/cli/diff_command.rbs +4 -0
  126. data/sig/rigor/cli/explain_command.rbs +4 -0
  127. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  128. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  129. data/sig/rigor/environment.rbs +8 -2
  130. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  131. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  132. data/sig/rigor/inference/builtins.rbs +2 -0
  133. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  134. data/sig/rigor/plugin/base.rbs +6 -0
  135. data/sig/rigor/plugin/blueprint.rbs +7 -0
  136. data/sig/rigor/plugin/fact_store.rbs +11 -0
  137. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  138. data/sig/rigor/plugin/load_error.rbs +6 -0
  139. data/sig/rigor/plugin/loader.rbs +20 -0
  140. data/sig/rigor/plugin/manifest.rbs +9 -0
  141. data/sig/rigor/plugin/registry.rbs +16 -0
  142. data/sig/rigor/plugin/services.rbs +3 -0
  143. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  144. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  145. data/sig/rigor/plugin.rbs +8 -0
  146. data/sig/rigor/scope.rbs +4 -2
  147. data/sig/rigor/type.rbs +28 -6
  148. data/sig/rigor.rbs +35 -2
  149. metadata +90 -1
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optionparser"
4
+
5
+ require_relative "../configuration"
6
+ require_relative "../sig_gen"
7
+
8
+ module Rigor
9
+ class CLI
10
+ # Executes the `rigor sig-gen` command — ADR-14 slices 1–3.
11
+ #
12
+ # Walks the given paths (or `configuration.paths` when none
13
+ # are supplied), classifies every reachable instance method
14
+ # via {Rigor::SigGen::Generator}, and either prints the
15
+ # resulting RBS skeletons / unified-style diffs (`--print`,
16
+ # `--diff`; slice 1) or writes them to the project signature
17
+ # tree via {Rigor::SigGen::Writer} (`--write`; slice 2).
18
+ #
19
+ # `--write` follows the established Ruby community
20
+ # convention: `lib/foo/bar.rb` → `sig/foo/bar.rbs`. New
21
+ # methods are inserted into the matching class declaration
22
+ # just before its closing `end`; new classes are appended
23
+ # to the file; non-existent target files are created. User-
24
+ # authored declarations are NEVER replaced unless
25
+ # `--overwrite` is set AND the candidate is a
26
+ # `tighter-return`.
27
+ #
28
+ # Parameter policy defaults to `untyped`. `--params=observed`
29
+ # (slice 3) opts in to caller-side observation harvesting:
30
+ # the `ObservationCollector` walks `--observe=PATH...`
31
+ # (default `spec/` when no flag is given AND a `spec/`
32
+ # directory exists), unions per-position arg types, and the
33
+ # generator emits the union per ADR-5 clause 2.
34
+ # `--params=observed-strict` stays reserved-but-inert until
35
+ # the capability-role catalog ships (rejected with a usage
36
+ # error so the surface stays stable).
37
+ class SigGenCommand
38
+ USAGE = "Usage: rigor sig-gen [options] [paths]"
39
+
40
+ VALID_MODES = %w[print diff write].freeze
41
+ VALID_PARAM_POLICIES = %w[untyped observed observed-strict].freeze
42
+ VALID_FORMATS = %w[text json].freeze
43
+
44
+ def initialize(argv:, out:, err:)
45
+ @argv = argv
46
+ @out = out
47
+ @err = err
48
+ end
49
+
50
+ # @return [Integer] CLI exit status.
51
+ def run
52
+ options = parse_options
53
+ return CLI::EXIT_USAGE if options.nil?
54
+
55
+ configuration = Configuration.load(options.fetch(:config))
56
+ paths = @argv.empty? ? configuration.paths : @argv
57
+
58
+ observations = collect_observations(configuration, options)
59
+ candidates = SigGen::Generator.new(configuration: configuration, paths: paths,
60
+ observations: observations,
61
+ include_private: options.fetch(:include_private)).run
62
+ mode = options.fetch(:mode).to_sym
63
+
64
+ if mode == :write
65
+ dispatch_write(candidates, configuration, options)
66
+ else
67
+ dispatch_print_or_diff(candidates, mode, options)
68
+ end
69
+ 0
70
+ end
71
+
72
+ private
73
+
74
+ def dispatch_print_or_diff(candidates, mode, options)
75
+ SigGen::Renderer.new(out: @out).render(
76
+ candidates: candidates,
77
+ mode: mode,
78
+ format: options.fetch(:format),
79
+ selection: options.fetch(:selection)
80
+ )
81
+ end
82
+
83
+ def dispatch_write(candidates, configuration, options)
84
+ layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
85
+ path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
86
+ writer = SigGen::Writer.new(path_mapper: path_mapper, overwrite: options.fetch(:overwrite))
87
+
88
+ results = writer.write_all(candidates)
89
+
90
+ SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format))
91
+ end
92
+
93
+ # Slice 3 — collect call-site argument observations when
94
+ # `--params=observed` is set. When `--observe=PATH` is
95
+ # not specified, default to `spec/` (skipped silently
96
+ # when the directory is absent).
97
+ def collect_observations(configuration, options)
98
+ return {} if options.fetch(:params) != "observed"
99
+
100
+ observe_paths = options.fetch(:observe)
101
+ observe_paths = ["spec"] if observe_paths.empty? && File.directory?("spec")
102
+ SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths).collect
103
+ end
104
+
105
+ def parse_options
106
+ options = {
107
+ mode: "print",
108
+ format: "text",
109
+ params: "untyped",
110
+ selection: [],
111
+ overwrite: false,
112
+ observe: [],
113
+ include_private: false,
114
+ config: nil
115
+ }
116
+ build_option_parser(options).parse!(@argv)
117
+
118
+ message = validation_error(options)
119
+ return options if message.nil?
120
+
121
+ @err.puts("sig-gen: #{message}")
122
+ nil
123
+ end
124
+
125
+ def build_option_parser(options) # rubocop:disable Metrics/AbcSize
126
+ OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
127
+ opts.banner = USAGE
128
+ opts.on("--print", "Write RBS skeletons to stdout (default)") { options[:mode] = "print" }
129
+ opts.on("--diff", "Write a unified diff against existing RBS") { options[:mode] = "diff" }
130
+ opts.on("--write", "Write generated RBS to sig/<path>.rbs files") { options[:mode] = "write" }
131
+ opts.on("--overwrite", "Allow tighter-return updates to replace user-authored RBS") do
132
+ options[:overwrite] = true
133
+ end
134
+ opts.on("--include-private", "Emit private / protected instance methods (default: public only)") do
135
+ options[:include_private] = true
136
+ end
137
+ opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
138
+ opts.on("--params=POLICY", "Parameter policy: untyped (default), observed, observed-strict") do |value|
139
+ options[:params] = value
140
+ end
141
+ opts.on("--observe=PATH", "Directory / file to scan for call-site observations (repeatable)") do |value|
142
+ options[:observe] << value
143
+ end
144
+ opts.on("--new-files", "Emit only new-file classifications") do
145
+ options[:selection] << SigGen::Classification::NEW_FILE
146
+ end
147
+ opts.on("--new-methods", "Emit only new-method classifications") do
148
+ options[:selection] << SigGen::Classification::NEW_METHOD
149
+ end
150
+ opts.on("--tighter-returns", "Emit only tighter-return classifications") do
151
+ options[:selection] << SigGen::Classification::TIGHTER_RETURN
152
+ end
153
+ opts.on("--config=PATH", "Path to the Rigor configuration file") { |value| options[:config] = value }
154
+ end
155
+ end
156
+
157
+ def validation_error(options)
158
+ mode = options.fetch(:mode)
159
+ format = options.fetch(:format)
160
+ params = options.fetch(:params)
161
+
162
+ return "--print, --diff, and --write are mutually exclusive flags; pick one" unless VALID_MODES.include?(mode)
163
+ return "unsupported --format=#{format}" unless VALID_FORMATS.include?(format)
164
+ return "unsupported --params=#{params}" unless VALID_PARAM_POLICIES.include?(params)
165
+ if params == "observed-strict"
166
+ return "--params=observed-strict is reserved until the capability-role catalog ships"
167
+ end
168
+
169
+ nil
170
+ end
171
+ end
172
+ end
173
+ end
@@ -61,7 +61,7 @@ module Rigor
61
61
  options
62
62
  end
63
63
 
64
- def execute(target:, options:) # rubocop:disable Metrics/AbcSize
64
+ def execute(target:, options:)
65
65
  file, line, column = target
66
66
  return 1 unless file_exists?(file)
67
67
 
@@ -10,7 +10,7 @@ module Rigor
10
10
  # branches share a single source of truth (the `Report` value object) so
11
11
  # the two formats stay in lockstep; that pairing is why this class is a
12
12
  # bit longer than the default class-length budget.
13
- class TypeScanRenderer # rubocop:disable Metrics/ClassLength
13
+ class TypeScanRenderer
14
14
  def initialize(out:)
15
15
  @out = out
16
16
  end
@@ -5,14 +5,14 @@ module Rigor
5
5
  # Aggregated report assembled by `TypeScanCommand` and consumed by
6
6
  # `TypeScanRenderer`. The struct holds per-file paths, accumulated
7
7
  # per-class counts, located fallback events, and any parse errors.
8
- Report = Data.define(
8
+ class Report < Data.define(
9
9
  :files,
10
10
  :parse_errors,
11
11
  :visits,
12
12
  :unrecognized,
13
13
  :events,
14
14
  :options
15
- ) do
15
+ )
16
16
  def visited_count
17
17
  visits.values.sum
18
18
  end
data/lib/rigor/cli.rb CHANGED
@@ -24,7 +24,8 @@ module Rigor
24
24
  "type-of" => :run_type_of,
25
25
  "type-scan" => :run_type_scan,
26
26
  "explain" => :run_explain,
27
- "diff" => :run_diff
27
+ "diff" => :run_diff,
28
+ "sig-gen" => :run_sig_gen
28
29
  }.freeze
29
30
 
30
31
  def self.start(argv = ARGV, out: $stdout, err: $stderr)
@@ -81,15 +82,36 @@ module Rigor
81
82
  runner = Analysis::Runner.new(
82
83
  configuration: configuration,
83
84
  explain: options.fetch(:explain),
84
- cache_store: cache_store
85
+ cache_store: cache_store,
86
+ collect_stats: options.fetch(:stats),
87
+ workers: resolve_workers(options, configuration)
85
88
  )
86
89
  result = runner.run(paths)
87
90
 
88
91
  write_result(result, options.fetch(:format))
92
+ write_run_stats(result.stats) if result.stats
89
93
  write_cache_stats(cache_root, runner.cache_store) if options.fetch(:cache_stats)
90
94
  result.success? ? 0 : 1
91
95
  end
92
96
 
97
+ # ADR-15 Phase 4c — resolves the worker count by
98
+ # precedence: CLI `--workers=N` (most explicit) > env
99
+ # `RIGOR_RACTOR_WORKERS` > config `.rigor.yml`
100
+ # `parallel.workers:` > 0 (sequential default). Returns
101
+ # an Integer; non-numeric values raise so typos fail
102
+ # loudly. CLI / env may pass a negative value — clamped
103
+ # to 0 (sequential) so a stray `-1` doesn't crash the
104
+ # pool spawn loop.
105
+ def resolve_workers(options, configuration)
106
+ cli_value = options[:workers]
107
+ return [Integer(cli_value), 0].max if cli_value
108
+
109
+ env_value = ENV.fetch("RIGOR_RACTOR_WORKERS", nil)
110
+ return [Integer(env_value), 0].max if env_value && !env_value.empty?
111
+
112
+ configuration.parallel_workers
113
+ end
114
+
93
115
  def parse_check_options
94
116
  options = {
95
117
  # `nil` triggers `Configuration.discover` (`.rigor.yml` then
@@ -99,7 +121,19 @@ module Rigor
99
121
  explain: false,
100
122
  cache_stats: false,
101
123
  clear_cache: false,
102
- no_cache: false
124
+ no_cache: false,
125
+ # Run-stats summary (target files, RBS class universe
126
+ # breakdown, wall time, peak RSS) is on by default
127
+ # because collection is ~free (single syscall for RSS,
128
+ # one walk of `class_decl_paths` for the breakdown).
129
+ # `--no-stats` suppresses it for callers that want a
130
+ # diagnostic-only output stream.
131
+ stats: true,
132
+ # ADR-15 Phase 4c — when nil, falls back to
133
+ # `RIGOR_RACTOR_WORKERS` then `.rigor.yml`
134
+ # `parallel.workers:` then 0 (sequential). See
135
+ # `resolve_workers` for the precedence chain.
136
+ workers: nil
103
137
  }
104
138
  parser = OptionParser.new do |opts|
105
139
  opts.banner = "Usage: rigor check [options] [paths]"
@@ -109,6 +143,14 @@ module Rigor
109
143
  opts.on("--cache-stats", "Print on-disk cache inventory at end of run") { options[:cache_stats] = true }
110
144
  opts.on("--clear-cache", "Remove the .rigor/cache directory before running") { options[:clear_cache] = true }
111
145
  opts.on("--no-cache", "Disable the persistent cache for this run") { options[:no_cache] = true }
146
+ opts.on("--[no-]stats",
147
+ "Print run summary (files, classes, memory, wall time) to stderr (default: on)") do |value|
148
+ options[:stats] = value
149
+ end
150
+ opts.on("--workers=N", Integer,
151
+ "Dispatch per-file analysis across N Ractor workers (default: 0; sequential)") do |value|
152
+ options[:workers] = value
153
+ end
112
154
  end
113
155
  parser.parse!(@argv)
114
156
  options
@@ -123,6 +165,15 @@ module Rigor
123
165
  end
124
166
  end
125
167
 
168
+ # Emits the {Analysis::RunStats} summary to STDERR so it
169
+ # doesn't interleave with the diagnostic stream (text or
170
+ # JSON) on STDOUT. JSON consumers can pipe stdout cleanly;
171
+ # interactive users still see the summary on their tty.
172
+ def write_run_stats(stats)
173
+ @err.puts("")
174
+ stats.format(@err)
175
+ end
176
+
126
177
  def write_cache_stats(cache_root, runtime_store)
127
178
  inv = Cache::Store.disk_inventory(root: cache_root)
128
179
 
@@ -279,6 +330,12 @@ module Rigor
279
330
  DiffCommand.new(argv: @argv, out: @out, err: @err).run
280
331
  end
281
332
 
333
+ def run_sig_gen
334
+ require_relative "cli/sig_gen_command"
335
+
336
+ SigGenCommand.new(argv: @argv, out: @out, err: @err).run
337
+ end
338
+
282
339
  def write_result(result, format)
283
340
  case format
284
341
  when "json"
@@ -318,6 +375,7 @@ module Rigor
318
375
  type-scan Report Scope#type_of coverage across PATHs
319
376
  explain Print the description of one or all CheckRules
320
377
  diff Compare current diagnostics to a saved baseline JSON
378
+ sig-gen Emit RBS skeletons inferred from .rb sources (ADR-14)
321
379
  version Print the Rigor version
322
380
  help Print this help
323
381
  HELP
@@ -13,7 +13,7 @@ module Rigor
13
13
  # is read, but no analyzer machinery consumes it yet. Slice 2
14
14
  # wires `Analysis::DependencySourceInference` against this
15
15
  # value object.
16
- class Dependencies # rubocop:disable Metrics/ClassLength
16
+ class Dependencies
17
17
  # Walking modes per
18
18
  # [ADR-10 § "Decision"](../../../docs/adr/10-dependency-source-inference.md#decision).
19
19
  VALID_MODES = %i[disabled when_missing full].freeze
@@ -61,7 +61,7 @@ module Rigor
61
61
  # walk time); `mode:` is one of {VALID_MODES}; `roots:` is
62
62
  # the list of subdirectories within the gem's installation
63
63
  # directory to walk (defaults to `["lib"]`).
64
- Entry = Data.define(:gem, :mode, :roots) do
64
+ class Entry < Data.define(:gem, :mode, :roots)
65
65
  def disabled? = mode == :disabled
66
66
  def when_missing? = mode == :when_missing
67
67
  def full? = mode == :full
@@ -63,6 +63,81 @@ module Rigor
63
63
  "dependencies" => {
64
64
  "source_inference" => [],
65
65
  "budget_per_gem" => Configuration::Dependencies::DEFAULT_BUDGET_PER_GEM
66
+ },
67
+ "parallel" => {
68
+ # ADR-15 Phase 4c — when greater than zero, `rigor check`
69
+ # dispatches per-file analysis across N Ractor workers
70
+ # built around {Rigor::Analysis::WorkerSession}.
71
+ # `0` (default) keeps the sequential coordinator path
72
+ # bit-for-bit unchanged. The CLI's `--workers=N` flag
73
+ # and the `RIGOR_RACTOR_WORKERS` env var both override
74
+ # this setting; precedence is CLI > env > config > 0.
75
+ "workers" => 0
76
+ },
77
+ "bundler" => {
78
+ # Open item O4 — target-project Bundler awareness.
79
+ # When `bundle_path:` is set (or auto-detected), Rigor
80
+ # walks `<bundle_path>/ruby/*/gems/*/sig/` and adds each
81
+ # gem-shipped sig directory to `signature_paths:`. With
82
+ # O7's failure-memo in place, conflicts (a vendored sig
83
+ # already declares the same constant) degrade gracefully
84
+ # to "no RBS env" with a single-line warning naming the
85
+ # offending file, rather than hanging.
86
+ #
87
+ # `bundle_path:` (String, optional): explicit path to the
88
+ # bundler install root (e.g., "vendor/bundle" or an
89
+ # absolute path). Resolved relative to the project root
90
+ # (`paths:`'s base) when relative.
91
+ #
92
+ # `auto_detect:` (Boolean, default true): when no
93
+ # explicit `bundle_path:` is set, try `.bundle/config`'s
94
+ # `BUNDLE_PATH:` first; fall back to `vendor/bundle/`
95
+ # under the project root if it exists. When neither is
96
+ # found, no extra sigs are added — the analyzer sees
97
+ # only rigor's vendored RBS and the user's
98
+ # `signature_paths:`.
99
+ #
100
+ # O4 Layer 3 keys:
101
+ #
102
+ # `lockfile:` (String, optional): explicit path to a
103
+ # `Gemfile.lock`. Resolved relative to the project root
104
+ # when relative. When set (or auto-detected via the
105
+ # `auto_detect:` flag below) Rigor parses the lockfile
106
+ # and uses it to FILTER the bundle-discovered `sig/`
107
+ # directories: only gems whose `(name, version,
108
+ # platform)` matches a lockfile entry are admitted to
109
+ # `signature_paths:`. Stale or out-of-band gems sitting
110
+ # in the bundle install tree are silently dropped.
111
+ #
112
+ # `auto_detect:` (Boolean, also gates the lockfile
113
+ # search): when true and `lockfile:` is nil, look for
114
+ # `<project_root>/Gemfile.lock`.
115
+ "bundle_path" => nil,
116
+ "auto_detect" => true,
117
+ "lockfile" => nil
118
+ },
119
+ "rbs_collection" => {
120
+ # Open item O4 Layer 3 slice 2 — `rbs collection
121
+ # install` awareness. When the target project has been
122
+ # set up with `rbs collection install`, the resulting
123
+ # `rbs_collection.lock.yaml` carries the resolved (gem,
124
+ # version, source) triples and `.gem_rbs_collection/`
125
+ # holds the downloaded `.rbs` files. Rigor parses the
126
+ # lockfile and auto-feeds each gem's
127
+ # `<collection_root>/<name>/<version>/` directory into
128
+ # `RbsLoader`'s `signature_paths:`. Sources of type
129
+ # `stdlib` are skipped because rigor's bundled
130
+ # `DEFAULT_LIBRARIES` already covers that surface.
131
+ #
132
+ # `lockfile:` (String, optional): explicit path to
133
+ # `rbs_collection.lock.yaml`. Resolved relative to the
134
+ # project root when relative.
135
+ #
136
+ # `auto_detect:` (Boolean, default true): when no
137
+ # explicit `lockfile:` is set, look for
138
+ # `<project_root>/rbs_collection.lock.yaml`.
139
+ "lockfile" => nil,
140
+ "auto_detect" => true
66
141
  }
67
142
  }.freeze
68
143
 
@@ -78,7 +153,9 @@ module Rigor
78
153
  :plugins_io_network, :plugins_io_allowed_paths,
79
154
  :plugins_io_allowed_url_hosts,
80
155
  :severity_profile, :severity_overrides,
81
- :dependencies
156
+ :dependencies, :parallel_workers,
157
+ :bundler_bundle_path, :bundler_auto_detect, :bundler_lockfile,
158
+ :rbs_collection_lockfile, :rbs_collection_auto_detect
82
159
 
83
160
  # Loads a configuration file.
84
161
  #
@@ -214,13 +291,13 @@ module Rigor
214
291
  private_class_method :load_with_includes, :merge_includes, :resolve_paths_in, :deep_merge,
215
292
  :merge_value, :merge_dependencies_hash
216
293
 
217
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
294
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
218
295
  def initialize(data = DEFAULTS)
219
296
  cache = DEFAULTS.fetch("cache").merge(data.fetch("cache", {}))
220
297
  plugins_io = DEFAULTS.fetch("plugins_io").merge(data.fetch("plugins_io", {}))
221
298
 
222
299
  @target_ruby = coerce_target_ruby(data.fetch("target_ruby", DEFAULTS.fetch("target_ruby")))
223
- @paths = Array(data.fetch("paths", DEFAULTS.fetch("paths"))).map(&:to_s)
300
+ @paths = Array(data.fetch("paths", DEFAULTS.fetch("paths"))).map(&:to_s).freeze
224
301
  user_excludes = Array(data.fetch("exclude", DEFAULTS.fetch("exclude"))).map(&:to_s)
225
302
  @exclude_patterns = (BUILTIN_EXCLUDES + user_excludes).uniq.freeze
226
303
  @plugins = Array(data.fetch("plugins", DEFAULTS.fetch("plugins"))).map do |entry|
@@ -246,10 +323,32 @@ module Rigor
246
323
  @dependencies = Dependencies.from_h(
247
324
  data.fetch("dependencies", DEFAULTS.fetch("dependencies"))
248
325
  )
326
+ parallel = DEFAULTS.fetch("parallel").merge(data.fetch("parallel", {}))
327
+ @parallel_workers = coerce_parallel_workers(parallel.fetch("workers"))
328
+ bundler = DEFAULTS.fetch("bundler").merge(data.fetch("bundler", {}))
329
+ bp = bundler.fetch("bundle_path")
330
+ @bundler_bundle_path = bp.nil? ? nil : bp.to_s.dup.freeze
331
+ @bundler_auto_detect = bundler.fetch("auto_detect") == true
332
+ lf = bundler.fetch("lockfile")
333
+ @bundler_lockfile = lf.nil? ? nil : lf.to_s.dup.freeze
334
+ rbs_collection = DEFAULTS.fetch("rbs_collection").merge(data.fetch("rbs_collection", {}))
335
+ rclf = rbs_collection.fetch("lockfile")
336
+ @rbs_collection_lockfile = rclf.nil? ? nil : rclf.to_s.dup.freeze
337
+ @rbs_collection_auto_detect = rbs_collection.fetch("auto_detect") == true
338
+ # Ractor migration Phase 2a: deep-freeze the
339
+ # Configuration so it is `Ractor.shareable?`. Every
340
+ # ivar above is now either a frozen value (Symbol /
341
+ # nil / Boolean) or an explicitly frozen
342
+ # collection / value object; freezing `self` makes the
343
+ # whole carrier safe to send across Ractor boundaries
344
+ # (and catches accidental post-init mutation in any
345
+ # caller). See
346
+ # `docs/design/20260514-ractor-migration.md`.
347
+ freeze
249
348
  end
250
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
349
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
251
350
 
252
- def to_h
351
+ def to_h # rubocop:disable Metrics/MethodLength
253
352
  {
254
353
  "target_ruby" => target_ruby,
255
354
  "paths" => paths,
@@ -269,7 +368,19 @@ module Rigor
269
368
  },
270
369
  "severity_profile" => severity_profile.to_s,
271
370
  "severity_overrides" => severity_overrides.to_h { |k, v| [k, v.to_s] },
272
- "dependencies" => dependencies.to_h
371
+ "dependencies" => dependencies.to_h,
372
+ "parallel" => {
373
+ "workers" => parallel_workers
374
+ },
375
+ "bundler" => {
376
+ "bundle_path" => bundler_bundle_path,
377
+ "auto_detect" => bundler_auto_detect,
378
+ "lockfile" => bundler_lockfile
379
+ },
380
+ "rbs_collection" => {
381
+ "lockfile" => rbs_collection_lockfile,
382
+ "auto_detect" => rbs_collection_auto_detect
383
+ }
273
384
  }
274
385
  end
275
386
 
@@ -327,6 +438,20 @@ module Rigor
327
438
  VALID_NETWORK_POLICIES = %i[disabled allowlist].freeze
328
439
  private_constant :VALID_NETWORK_POLICIES
329
440
 
441
+ # ADR-15 Phase 4c — accepts a non-negative Integer (or a
442
+ # string-shaped one from YAML files that miss type
443
+ # annotations). Negative / non-integer values raise so
444
+ # typos / bad YAML fail loudly rather than silently
445
+ # disabling parallelism.
446
+ def coerce_parallel_workers(value)
447
+ integer = Integer(value)
448
+ raise ArgumentError, "parallel.workers must be >= 0, got #{value.inspect}" if integer.negative?
449
+
450
+ integer
451
+ rescue TypeError, ArgumentError => e
452
+ raise ArgumentError, "parallel.workers must be a non-negative Integer, got #{value.inspect} (#{e.message})"
453
+ end
454
+
330
455
  def coerce_network_policy(value)
331
456
  sym = value.to_sym
332
457
  unless VALID_NETWORK_POLICIES.include?(sym)