rigortype 0.2.9 → 0.3.1

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 (222) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/builtins/ruby_core/array.yml +416 -392
  4. data/data/builtins/ruby_core/file.yml +42 -42
  5. data/data/builtins/ruby_core/hash.yml +302 -302
  6. data/data/builtins/ruby_core/io.yml +191 -191
  7. data/data/builtins/ruby_core/numeric.yml +321 -366
  8. data/data/builtins/ruby_core/proc.yml +124 -124
  9. data/data/builtins/ruby_core/range.yml +21 -21
  10. data/data/builtins/ruby_core/rational.yml +39 -39
  11. data/data/builtins/ruby_core/re.yml +65 -65
  12. data/data/builtins/ruby_core/set.yml +106 -106
  13. data/data/builtins/ruby_core/struct.yml +14 -14
  14. data/data/core_overlay/string_scanner.rbs +6 -5
  15. data/docs/handbook/01-getting-started.md +22 -34
  16. data/docs/handbook/03-narrowing.md +2 -1
  17. data/docs/handbook/04-tuples-and-shapes.md +7 -3
  18. data/docs/handbook/06-classes.md +19 -10
  19. data/docs/handbook/07-rbs-and-extended.md +76 -101
  20. data/docs/handbook/08-understanding-errors.md +114 -246
  21. data/docs/handbook/09-plugins.md +54 -144
  22. data/docs/handbook/11-sig-gen.md +11 -1
  23. data/docs/handbook/README.md +5 -3
  24. data/docs/handbook/appendix-liskov.md +4 -2
  25. data/docs/handbook/appendix-mypy.md +4 -3
  26. data/docs/handbook/appendix-phpstan.md +14 -7
  27. data/docs/handbook/appendix-steep.md +4 -2
  28. data/docs/handbook/appendix-type-theory.md +3 -1
  29. data/docs/install.md +14 -6
  30. data/docs/manual/01-installation.md +98 -4
  31. data/docs/manual/02-cli-reference.md +81 -12
  32. data/docs/manual/03-configuration.md +34 -2
  33. data/docs/manual/04-diagnostics.md +59 -4
  34. data/docs/manual/06-baseline.md +35 -1
  35. data/docs/manual/07-plugins.md +4 -4
  36. data/docs/manual/08-skills.md +6 -1
  37. data/docs/manual/09-editor-integration.md +7 -6
  38. data/docs/manual/11-ci.md +51 -0
  39. data/docs/manual/12-caching.md +65 -0
  40. data/docs/manual/plugins/rigor-actioncable.md +32 -0
  41. data/docs/manual/plugins/rigor-devise.md +4 -2
  42. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
  43. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
  45. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
  46. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  47. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
  48. data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
  49. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  50. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
  52. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
  53. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +112 -0
  54. data/lib/rigor/analysis/check_rules.rb +581 -98
  55. data/lib/rigor/analysis/dependency_recorder.rb +93 -9
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  57. data/lib/rigor/analysis/incremental_session.rb +456 -51
  58. data/lib/rigor/analysis/path_expansion.rb +42 -0
  59. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
  60. data/lib/rigor/analysis/rule_catalog.rb +226 -1
  61. data/lib/rigor/analysis/run_cache_key.rb +78 -0
  62. data/lib/rigor/analysis/run_cache_probe.rb +72 -0
  63. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
  64. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
  65. data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
  66. data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
  67. data/lib/rigor/analysis/runner.rb +344 -68
  68. data/lib/rigor/analysis/severity_stamp.rb +43 -0
  69. data/lib/rigor/analysis/worker_session.rb +30 -2
  70. data/lib/rigor/bleeding_edge.rb +37 -8
  71. data/lib/rigor/builtins/regex_refinement.rb +36 -1
  72. data/lib/rigor/cache/descriptor.rb +88 -28
  73. data/lib/rigor/cache/file_digest.rb +158 -0
  74. data/lib/rigor/cache/incremental_snapshot.rb +41 -5
  75. data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
  76. data/lib/rigor/cache/rbs_descriptor.rb +56 -10
  77. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
  78. data/lib/rigor/cache/store.rb +132 -27
  79. data/lib/rigor/ci_detector.rb +90 -0
  80. data/lib/rigor/cli/annotate_command.rb +15 -9
  81. data/lib/rigor/cli/baseline_command.rb +4 -1
  82. data/lib/rigor/cli/check_command.rb +223 -32
  83. data/lib/rigor/cli/check_invocation.rb +84 -0
  84. data/lib/rigor/cli/coverage_command.rb +6 -1
  85. data/lib/rigor/cli/docs_command.rb +0 -30
  86. data/lib/rigor/cli/doctor_command.rb +169 -31
  87. data/lib/rigor/cli/lsp_command.rb +5 -0
  88. data/lib/rigor/cli/mcp_command.rb +5 -0
  89. data/lib/rigor/cli/plugins_command.rb +36 -6
  90. data/lib/rigor/cli/plugins_renderer.rb +44 -12
  91. data/lib/rigor/cli/probe_environment.rb +85 -0
  92. data/lib/rigor/cli/sig_gen_command.rb +36 -9
  93. data/lib/rigor/cli/skill_command.rb +22 -33
  94. data/lib/rigor/cli/skill_deep_probe.rb +172 -0
  95. data/lib/rigor/cli/skill_describe.rb +75 -9
  96. data/lib/rigor/cli/trace_command.rb +5 -6
  97. data/lib/rigor/cli/type_of_command.rb +9 -8
  98. data/lib/rigor/cli/type_scan_command.rb +8 -8
  99. data/lib/rigor/cli.rb +15 -4
  100. data/lib/rigor/config_audit.rb +48 -2
  101. data/lib/rigor/configuration/severity_profile.rb +31 -0
  102. data/lib/rigor/configuration.rb +156 -8
  103. data/lib/rigor/environment/default_libraries.rb +36 -0
  104. data/lib/rigor/environment/rbs_loader.rb +164 -5
  105. data/lib/rigor/environment.rb +29 -28
  106. data/lib/rigor/inference/budget_trace.rb +77 -2
  107. data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
  108. data/lib/rigor/inference/def_handle.rb +23 -0
  109. data/lib/rigor/inference/def_node_resolver.rb +90 -0
  110. data/lib/rigor/inference/def_return_typer.rb +2 -1
  111. data/lib/rigor/inference/expression_typer.rb +361 -113
  112. data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
  113. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
  114. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
  115. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
  116. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +17 -1
  117. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
  118. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
  119. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
  120. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
  121. data/lib/rigor/inference/method_dispatcher.rb +57 -1
  122. data/lib/rigor/inference/mutation_widening.rb +42 -8
  123. data/lib/rigor/inference/narrowing.rb +127 -17
  124. data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
  125. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  126. data/lib/rigor/inference/rbs_type_translator.rb +8 -1
  127. data/lib/rigor/inference/scope_indexer.rb +451 -36
  128. data/lib/rigor/inference/statement_evaluator.rb +64 -15
  129. data/lib/rigor/inference/struct_fold_safety.rb +48 -11
  130. data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
  131. data/lib/rigor/inference/void_origin.rb +25 -0
  132. data/lib/rigor/inference/void_tail_summary.rb +220 -0
  133. data/lib/rigor/language_server/buffer_resolution.rb +6 -3
  134. data/lib/rigor/language_server/buffer_table.rb +46 -6
  135. data/lib/rigor/language_server/completion_provider.rb +2 -1
  136. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
  137. data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
  138. data/lib/rigor/language_server/folding_range_provider.rb +2 -1
  139. data/lib/rigor/language_server/incremental_sync.rb +159 -0
  140. data/lib/rigor/language_server/selection_range_provider.rb +2 -1
  141. data/lib/rigor/language_server/server.rb +19 -9
  142. data/lib/rigor/language_server/signature_help_provider.rb +2 -1
  143. data/lib/rigor/language_server.rb +1 -0
  144. data/lib/rigor/plugin/base.rb +38 -24
  145. data/lib/rigor/plugin/inflector.rb +12 -3
  146. data/lib/rigor/plugin/io_boundary.rb +5 -1
  147. data/lib/rigor/plugin/isolation.rb +81 -11
  148. data/lib/rigor/plugin/load_error.rb +10 -1
  149. data/lib/rigor/plugin/loader.rb +96 -14
  150. data/lib/rigor/plugin/node_rule_walk.rb +5 -3
  151. data/lib/rigor/plugin/registry.rb +32 -23
  152. data/lib/rigor/plugin.rb +26 -0
  153. data/lib/rigor/protection/mutator.rb +3 -2
  154. data/lib/rigor/reflection.rb +64 -0
  155. data/lib/rigor/runtime/jit.rb +128 -0
  156. data/lib/rigor/scope/discovery_index.rb +12 -2
  157. data/lib/rigor/scope.rb +122 -28
  158. data/lib/rigor/sig_gen/classification.rb +5 -1
  159. data/lib/rigor/sig_gen/generator.rb +38 -6
  160. data/lib/rigor/sig_gen/observation_collector.rb +4 -3
  161. data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
  162. data/lib/rigor/sig_gen/renderer.rb +10 -0
  163. data/lib/rigor/sig_gen/write_result.rb +9 -4
  164. data/lib/rigor/sig_gen/writer.rb +207 -49
  165. data/lib/rigor/source/node_children.rb +116 -0
  166. data/lib/rigor/source/node_locator.rb +3 -1
  167. data/lib/rigor/source/node_walker.rb +4 -2
  168. data/lib/rigor/source.rb +1 -0
  169. data/lib/rigor/type/difference.rb +28 -24
  170. data/lib/rigor/type/hash_shape.rb +34 -10
  171. data/lib/rigor/version.rb +1 -1
  172. data/lib/rigortype.rb +24 -0
  173. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
  174. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
  175. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
  176. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
  177. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
  178. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
  179. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
  180. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
  181. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
  182. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
  183. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
  184. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
  185. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
  186. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
  187. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
  188. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
  189. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
  190. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
  191. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
  192. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
  193. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
  194. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
  195. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
  196. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
  197. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
  198. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
  199. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
  200. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
  201. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
  202. data/sig/prism_node_children.rbs +9 -0
  203. data/sig/rigor/cache.rbs +6 -0
  204. data/sig/rigor/inference/void_origin.rbs +18 -0
  205. data/sig/rigor/inference.rbs +7 -0
  206. data/sig/rigor/plugin/base.rbs +6 -6
  207. data/sig/rigor/reflection.rbs +1 -0
  208. data/sig/rigor/scope.rbs +12 -1
  209. data/skills/rigor-ci-setup/SKILL.md +10 -0
  210. data/skills/rigor-editor-setup/SKILL.md +11 -0
  211. data/skills/rigor-next-steps/SKILL.md +23 -2
  212. data/skills/rigor-plugin-author/SKILL.md +3 -3
  213. data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
  214. data/skills/rigor-plugin-review/SKILL.md +2 -2
  215. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
  216. data/skills/rigor-project-init/SKILL.md +29 -3
  217. data/skills/rigor-project-init/references/01-detect.md +1 -1
  218. data/skills/rigor-project-init/references/02-configure.md +39 -5
  219. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
  220. data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
  221. metadata +48 -26
  222. data/lib/rigor/cli/ci_detector.rb +0 -89
@@ -9,11 +9,13 @@ require_relative "../configuration"
9
9
  require_relative "options"
10
10
  require_relative "../environment"
11
11
  require_relative "../scope"
12
+ require_relative "../source/node_children"
12
13
  require_relative "../inference/def_return_typer"
13
14
  require_relative "../inference/scope_indexer"
14
15
  require_relative "../inference/statement_evaluator"
15
16
  require_relative "prism_colorizer"
16
17
  require_relative "command"
18
+ require_relative "probe_environment"
17
19
 
18
20
  module Rigor
19
21
  class CLI
@@ -105,7 +107,7 @@ module Rigor
105
107
  # bindings, so a loop-body line annotates the joined widened type (`Integer`) rather than a stale
106
108
  # first-iterations constant (`1 | 2`).
107
109
  scope_index = Inference::ScopeIndexer.index(
108
- parse_result.value, default_scope: base_scope(configuration),
110
+ parse_result.value, default_scope: base_scope(configuration, file),
109
111
  converged_loop_recording: true
110
112
  )
111
113
  line_types = LineTypeCollector.new(scope_index).collect(parse_result.value)
@@ -128,12 +130,12 @@ module Rigor
128
130
  @out.puts(JSON.generate({ "annotations" => annotations }))
129
131
  end
130
132
 
131
- def base_scope(configuration)
133
+ # The plugin-aware environment for the annotated file, so a `#=> <type>` matches what `rigor check`
134
+ # infers on that line — including types synthesized from the file's own inline RBS annotations (the file
135
+ # is threaded as the synthesizer's `source_files:`; see {ProbeEnvironment}).
136
+ def base_scope(configuration, file)
132
137
  Scope.empty(
133
- environment: Environment.for_project(
134
- libraries: configuration.libraries,
135
- signature_paths: configuration.signature_paths
136
- )
138
+ environment: ProbeEnvironment.build(configuration: configuration, source_files: [file])
137
139
  )
138
140
  end
139
141
 
@@ -247,14 +249,18 @@ module Rigor
247
249
  block.call(stmt)
248
250
  end
249
251
  else
250
- node.compact_child_nodes.each { |child| each_statement(child, &block) }
252
+ node.rigor_each_child { |child| each_statement(child, &block) }
251
253
  end
252
254
  end
253
255
 
254
256
  # For a line no statement closes (the `if` / block header lines), fall back to the widest expression ending there.
257
+ # A hash-pair node (`1 => 2,` inside a multi-line literal / keyword-argument list) is not an expression — typing
258
+ # it only echoes the `Dynamic[top]` fallback — so those interior lines stay bare and the literal's type appears
259
+ # once, on the line the enclosing statement closes.
255
260
  def fill_uncovered_lines(program, by_line)
256
261
  widest_per_line(program).each do |line, node|
257
262
  next if by_line.key?(line)
263
+ next if node.is_a?(Prism::AssocNode) || node.is_a?(Prism::AssocSplatNode)
258
264
 
259
265
  type = node.is_a?(Prism::BlockParametersNode) ? block_params_type(node) : type_of(node)
260
266
  by_line[line] = type unless type.nil?
@@ -298,7 +304,7 @@ module Rigor
298
304
  return if node.nil?
299
305
 
300
306
  block.call(node)
301
- node.compact_child_nodes.each { |child| walk(child, &block) }
307
+ node.rigor_each_child { |child| walk(child, &block) }
302
308
  end
303
309
 
304
310
  # Types the node through the flow evaluator (not the bare expression typer) under its recorded entry scope, so
@@ -331,7 +337,7 @@ module Rigor
331
337
  return if node.nil?
332
338
 
333
339
  block.call(node) if node.is_a?(Prism::DefNode)
334
- node.compact_child_nodes.each { |child| each_def_node(child, &block) }
340
+ node.rigor_each_child { |child| each_def_node(child, &block) }
335
341
  end
336
342
  end
337
343
  end
@@ -87,8 +87,11 @@ module Rigor
87
87
  "match-mode: #{options.fetch(:match_mode)})"
88
88
  )
89
89
  if configuration.baseline_path.nil?
90
+ # Name the config file that actually governs this project (`.rigor.dist.yml` when no local override
91
+ # exists), so the hint points at the file the user must edit.
92
+ config_name = options.fetch(:config) || Configuration.discover || ".rigor.yml"
90
93
  @err.puts(
91
- "rigor: note — `.rigor.yml` does not declare `baseline:`; " \
94
+ "rigor: note — `#{config_name}` does not declare `baseline:`; " \
92
95
  "add `baseline: #{path}` to activate the suppression."
93
96
  )
94
97
  end
@@ -8,12 +8,17 @@ require_relative "../configuration"
8
8
  require_relative "../config_audit"
9
9
  require_relative "../analysis/result"
10
10
  require_relative "../analysis/rule_catalog"
11
- require_relative "coverage_scan"
11
+ # The baseline filter runs on EVERY exit path — including the ADR-87 WD4 cache-HIT fast path, which skips
12
+ # `load_check_dependencies` — so it must be a load-time require. It pulls only YAML, never the engine.
13
+ require_relative "../analysis/baseline"
14
+ require_relative "../runtime/jit"
12
15
  require_relative "command"
13
16
  require_relative "options"
14
17
  require_relative "diagnostic_formats"
15
- require_relative "ci_detector"
16
- require_relative "check_runner_factory"
18
+ require_relative "../ci_detector"
19
+ # ADR-87 WD4 — `coverage_scan` and `check_runner_factory` both pull the inference engine, so they are required
20
+ # lazily (in `compute_coverage` / `build_check_runner`) rather than at load time: an ordinary check whose
21
+ # result the run-cache probe serves must reach the hit verdict without `rigor/inference` in $LOADED_FEATURES.
17
22
 
18
23
  module Rigor
19
24
  class CLI
@@ -29,26 +34,43 @@ module Rigor
29
34
  # concerns that are clearer read together than split across micro-classes.
30
35
  class CheckCommand < Command # rubocop:disable Metrics/ClassLength
31
36
  # @return [Integer] CLI exit status.
32
- def run # rubocop:disable Metrics/AbcSize
33
- load_check_dependencies
37
+ def run # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
38
+ # Arm deferred YJIT enablement before any analysis work: the deadline
39
+ # thread only fires once a run outlasts the amortization window, so a
40
+ # short check finishes before it ever pays JIT compile cost while a
41
+ # long run JITs its dominant tail (Runtime::Jit).
42
+ Runtime::Jit.enable_after(Runtime::Jit.deadline_seconds)
43
+ # ADR-87 WD4 — parse options + resolve config WITHOUT the inference engine, so the run-cache hit probe
44
+ # can run first. The heavy engine (`load_check_dependencies`) loads only on a miss / non-cacheable run.
34
45
  options = parse_check_options
35
46
  buffer = Options.resolve_buffer_binding(options, err: @err)
36
47
  return CLI::EXIT_USAGE if buffer == :usage_error
37
48
 
38
49
  configuration = load_check_configuration(options)
39
50
  configuration = apply_bleeding_edge_override(configuration, options)
51
+ conflict = parameter_inference_incremental_conflict(configuration, options)
52
+ return conflict unless conflict.nil?
53
+
40
54
  config_warnings = warn_unresolved_config(configuration)
41
55
  cache_root = configuration.cache_path
42
56
  handle_clear_cache(cache_root) if options.fetch(:clear_cache)
43
57
 
58
+ # ADR-87 WD4 — try to serve the whole run from the ADR-45 cache before booting the engine. A hit boots
59
+ # only CLI + config + cache + digest code (no `rigor/inference`), skipping the plugin prepass + env
60
+ # build entirely.
61
+ probed = try_run_cache_hit(configuration, options, buffer, cache_root)
62
+ return finalize_cache_hit(probed, configuration, options, config_warnings) unless probed.nil?
63
+
64
+ load_check_dependencies
44
65
  special = dispatch_special_check_mode(configuration, options, cache_root)
45
66
  return special unless special.nil?
46
67
 
47
- runner = build_check_runner(
68
+ invocation = invoke_check(
48
69
  configuration: configuration, options: options,
49
70
  buffer: buffer, cache_root: cache_root
50
71
  )
51
- raw_result = runner.run(@argv.empty? ? configuration.paths : @argv)
72
+ runner = invocation.runner
73
+ raw_result = invocation.result
52
74
  result = apply_baseline_filter(raw_result, configuration, options)
53
75
 
54
76
  coverage = compute_coverage(runner, configuration, options)
@@ -66,23 +88,113 @@ module Rigor
66
88
 
67
89
  private
68
90
 
91
+ # ADR-87 WD4 — attempt the boot-slimming run-cache hit. Returns the cached {Analysis::Result} (severity
92
+ # profile applied, no stats — matching a cache-served `Runner#run`) on a hit, or nil to fall through to
93
+ # the full engine path. Only an ordinary sequential check whose result IS cache-served the same way is
94
+ # eligible; every other mode (below) declines so the full path handles it unchanged.
95
+ def try_run_cache_hit(configuration, options, buffer, cache_root)
96
+ return nil unless run_cache_hit_eligible?(configuration, options, buffer)
97
+
98
+ require_relative "../analysis/run_cache_probe"
99
+ Analysis::RunCacheProbe.new(
100
+ configuration: configuration, cache_root: cache_root, explain: options.fetch(:explain)
101
+ ).serve(@argv.empty? ? configuration.paths : @argv)
102
+ end
103
+
104
+ # The probe is eligible only for a run the ADR-45 result cache actually serves: a writable cache (not
105
+ # `--no-cache`), no editor buffer, sequential (pool runs are not result-cacheable), and no mode needing
106
+ # the engine (`--coverage` / `--incremental` / `--verify-incremental`) or the runner's own reporting
107
+ # (`--cache-stats`, the `RIGOR_*_TRACE` dev probes). A wrongly-permitted run still MISSES (the entry was
108
+ # never written its way) and falls through — the gate is an optimisation, never a soundness boundary.
109
+ def run_cache_hit_eligible?(configuration, options, buffer)
110
+ buffer.nil? &&
111
+ !options.fetch(:no_cache) && !options.fetch(:coverage) && !options.fetch(:cache_stats) &&
112
+ !options.fetch(:incremental) && !options.fetch(:verify_incremental) &&
113
+ !pool_workers_configured?(options, configuration) &&
114
+ ENV["RIGOR_BUDGET_TRACE"].to_s.empty? && ENV["RIGOR_HEAP_TRACE"].to_s.empty?
115
+ end
116
+
117
+ # Any signal that per-file analysis would run in a worker pool (CLI `--workers`, the env override, or the
118
+ # config default). Deliberately over-declines (a false positive only forgoes the fast lane); mirrors
119
+ # {CheckRunnerFactory.resolve_workers} without requiring it (that pulls in the engine).
120
+ def pool_workers_configured?(options, configuration)
121
+ cli = options[:workers]
122
+ return Integer(cli).positive? if cli
123
+
124
+ env = ENV.fetch("RIGOR_RACTOR_WORKERS", nil)
125
+ return Integer(env).positive? if env && !env.empty?
126
+
127
+ configuration.parallel_workers.positive?
128
+ rescue ArgumentError
129
+ false
130
+ end
131
+
132
+ # ADR-87 WD4 — the engine-free tail for a cache-served hit: baseline filter + output + exit code,
133
+ # mirroring the full path's tail but without stats (a hit collected none), trace appendices (they read
134
+ # engine counters), or eviction (deferred to the next miss). `raw_result` is pre-baseline so the strict
135
+ # gate reads the same diagnostics the full path would.
136
+ def finalize_cache_hit(raw_result, configuration, options, config_warnings)
137
+ result = apply_baseline_filter(raw_result, configuration, options)
138
+ write_result(result, options.fetch(:format), config_warnings: config_warnings)
139
+ emit_ci_detected_output(result, options)
140
+
141
+ exit_code = result.success? ? 0 : 1
142
+ exit_code = 1 if baseline_strict_violation?(raw_result.diagnostics, configuration, options)
143
+ exit_code
144
+ end
145
+
146
+ # ADR-67 WD6c — `parameter_inference:` and the ADR-46 incremental modes are mutually exclusive in slice 1.
147
+ # The parameter table introduces cross-file edges (a caller's argument types drive a callee's body
148
+ # diagnostics) that the per-file dependency recorder does not yet carry, so a cached file whose parameter
149
+ # seeds changed would serve a stale result. Refuse with a message naming the ADR-46 gap rather than
150
+ # silently producing unsound incremental diagnostics; the edge wiring is the named follow-up. Returns the
151
+ # usage exit code on a conflict, or nil to proceed. `--verify-incremental` (the acceptance gate that runs
152
+ # incremental analysis) is caught here too.
153
+ def parameter_inference_incremental_conflict(configuration, options)
154
+ return nil unless configuration.parameter_inference
155
+ return nil unless options.fetch(:incremental) || options.fetch(:verify_incremental)
156
+
157
+ flag = options.fetch(:incremental) ? "--incremental" : "--verify-incremental"
158
+ @err.puts("rigor: parameter_inference: cannot combine with #{flag} — the call-site parameter table " \
159
+ "introduces cross-file caller→callee edges the ADR-46 incremental dependency graph does not " \
160
+ "yet record, so a cached file could serve a stale diagnostic. Run a full check (drop " \
161
+ "#{flag}), or disable parameter_inference: for this run.")
162
+ CLI::EXIT_USAGE
163
+ end
164
+
69
165
  # ADR-46 — the two incremental-analysis check modes both fully handle the run and return an exit code (so `run`
70
166
  # short-circuits); returns nil for an ordinary check.
71
167
  def dispatch_special_check_mode(configuration, options, cache_root)
72
- return run_verify_incremental(configuration) if options.fetch(:verify_incremental)
168
+ return run_verify_incremental(configuration, options, cache_root) if options.fetch(:verify_incremental)
73
169
  return run_incremental_check(configuration, options, cache_root) if options.fetch(:incremental)
74
170
 
75
171
  nil
76
172
  end
77
173
 
174
+ # ADR-85 WD1 — the persistent cache the incremental session threads into each internal Runner so plugin
175
+ # `#prepare` producers (the ADR-9/#74/ADR-60 WD3 record-and-validate caches) and the RBS environment serve
176
+ # from disk across processes / rechecks rather than recomputing every invocation. Honors `--no-cache` (nil
177
+ # = no store), mirroring {CheckRunnerFactory}. The whole-run ADR-45 result cache stays inert on these runs
178
+ # — `Runner#run_result_cacheable?` excludes `record_dependencies` / `analyze_only`.
179
+ def incremental_cache_store(configuration, options, cache_root)
180
+ return nil if options.fetch(:no_cache)
181
+
182
+ Cache::Store.new(root: cache_root, max_bytes: configuration.cache_max_bytes)
183
+ end
184
+
78
185
  # ADR-46 — the incremental-analysis acceptance gate. Runs a baseline analysis (recording cross-file dependencies),
79
186
  # then re-analyzes a representative subset of files and serves the rest from the per-file cache (the body tier),
80
187
  # and asserts the merged diagnostics are byte-identical to a full `--no-cache` analysis. A mismatch means the
81
188
  # incremental machinery would serve a stale — manufactured — diagnostic, the soundness failure this gate exists to
82
189
  # catch. Prints a one-line PASS (exit 0) or the differing diagnostics (exit 1).
83
- def run_verify_incremental(configuration)
190
+ def run_verify_incremental(configuration, options, cache_root)
84
191
  paths = @argv.empty? ? nil : @argv
85
- session = Analysis::IncrementalSession.new(configuration: configuration, paths: paths)
192
+ # ADR-85 WD1 thread the store so the gate also asserts the cache-served producer path stays
193
+ # byte-identical to the uncached full run (`verify_full_diagnostics` builds a nil-store runner).
194
+ session = Analysis::IncrementalSession.new(
195
+ configuration: configuration, paths: paths,
196
+ cache_store: incremental_cache_store(configuration, options, cache_root)
197
+ )
86
198
  session.baseline
87
199
  analyzed = session.analyzed_files
88
200
 
@@ -101,18 +213,30 @@ module Rigor
101
213
  # updated snapshot for the next invocation. Diagnostics are identical to a full run (the `--verify-incremental`
102
214
  # gate enforces this); the win is skipping per-file inference for unchanged files.
103
215
  def run_incremental_check(configuration, options, cache_root)
216
+ require_relative "check_runner_factory"
104
217
  paths = @argv.empty? ? nil : @argv
105
- probe = Analysis::Runner.new(configuration: configuration, cache_store: nil)
106
- files = paths ? probe.analysis_file_set(paths) : probe.analysis_file_set
107
218
  fingerprint = Cache::IncrementalSnapshot.fingerprint(
108
219
  configuration: configuration, roots: paths || configuration.paths
109
220
  )
110
221
  snapshot = Cache::IncrementalSnapshot.new(root: cache_root)
111
- session = Analysis::IncrementalSession.new(configuration: configuration, paths: paths)
222
+ store = incremental_cache_store(configuration, options, cache_root)
223
+ session = Analysis::IncrementalSession.new(
224
+ configuration: configuration, paths: paths,
225
+ cache_store: store,
226
+ # ADR-46 — thread the same worker-count precedence the standard `check` path uses
227
+ # (CLI `--workers` > `RIGOR_RACTOR_WORKERS` > `parallel.workers:` > 0) so the recheck's closure
228
+ # re-analysis parallelises; the fork pool marshals dependency records back so the graph is sound.
229
+ workers: CheckRunnerFactory.resolve_workers(options, configuration)
230
+ )
112
231
 
113
232
  diagnostics, warm = session.run_incremental(snapshot: snapshot, fingerprint: fingerprint)
233
+ # The banner's file count comes from the session's analyzed set (cold analyses all; a warm recheck's
234
+ # `@analyzed` advances to the current file set), so a dedicated probe Runner + `Dir.glob` is no longer
235
+ # built just to size the banner (recon §3 — the analysis tree was expanded three times per recheck).
114
236
  @err.puts("rigor: --incremental #{warm ? 'warm — reused cached diagnostics' : 'cold — full analysis'} " \
115
- "(#{files.size} files)")
237
+ "(#{session.analyzed_files.size} files)")
238
+ emit_incremental_fact_surface_notes(session)
239
+ write_incremental_cache_stats(session, cache_root, store) if options.fetch(:cache_stats)
116
240
 
117
241
  result = apply_baseline_filter(Analysis::Result.new(diagnostics: diagnostics, stats: nil), configuration,
118
242
  options)
@@ -120,6 +244,40 @@ module Rigor
120
244
  result.success? ? 0 : 1
121
245
  end
122
246
 
247
+ # ADR-88 WD1 — a one-line stderr note when the plugin fact surface (an ADR-9 fact, an ADR-60 producer
248
+ # value, or an `incremental_state_fingerprint` hook) forced a full run: either it CHANGED since the
249
+ # snapshot (a Sorbet sig edit, a schema change) or a contributing plugin declares NO fingerprint surface
250
+ # (opaque — a full run every invocation). Both are the sound direction; the note explains why a recheck
251
+ # was not warm.
252
+ def emit_incremental_fact_surface_notes(session)
253
+ opaque = session.opaque_plugin_ids
254
+ unless opaque.empty?
255
+ @err.puts("rigor: --incremental plugin#{'s' unless opaque.size == 1} #{opaque.sort.join(', ')} " \
256
+ "contribute call-site types with no incremental fingerprint surface; the snapshot cannot " \
257
+ "be reused (full analysis every run).")
258
+ end
259
+ return unless session.fact_surface_invalidated? && opaque.empty?
260
+
261
+ @err.puts("rigor: --incremental plugin fact surface changed since the snapshot; ran a full analysis.")
262
+ end
263
+
264
+ # ADR-88 WD1 — `--incremental --cache-stats` surfaces the on-disk cache inventory (as a plain `check`
265
+ # does) plus the fact-surface status line, so an operator can see whether a full run was fact-surface
266
+ # driven. The plain `check` cache-stats path is bypassed by the incremental short-circuit, so it is
267
+ # emitted here.
268
+ def write_incremental_cache_stats(session, cache_root, store)
269
+ write_cache_stats(cache_root, store)
270
+ status =
271
+ if !session.opaque_plugin_ids.empty?
272
+ "opaque (#{session.opaque_plugin_ids.sort.join(', ')})"
273
+ elsif session.fact_surface_invalidated?
274
+ "changed — snapshot invalidated"
275
+ else
276
+ "unchanged"
277
+ end
278
+ @out.puts(" plugin fact surface: #{status}")
279
+ end
280
+
123
281
  def verify_full_diagnostics(configuration, paths)
124
282
  runner = Analysis::Runner.new(configuration: configuration, cache_store: nil)
125
283
  (paths ? runner.run(paths) : runner.run).diagnostics
@@ -226,10 +384,16 @@ module Rigor
226
384
  @err.puts("rigor: #{silenced_count} diagnostic(s) silenced by baseline #{baseline_path}")
227
385
  end
228
386
 
229
- def build_check_runner(configuration:, options:, buffer:, cache_root:)
230
- CheckRunnerFactory.build(
387
+ # The primary check run, routed through the shared {CheckInvocation} entry point so `rigor doctor` and
388
+ # `rigor skill describe --deep` reach a result through this exact path rather than re-deriving it (#148).
389
+ # Required lazily for the reason the old inline `CheckRunnerFactory.build` was: it pulls the inference engine,
390
+ # and ADR-87 WD4's cache-hit fast path must reach its verdict without it.
391
+ def invoke_check(configuration:, options:, buffer:, cache_root:)
392
+ require_relative "check_invocation"
393
+ CheckInvocation.run(
231
394
  configuration: configuration, options: options,
232
- buffer: buffer, cache_root: cache_root
395
+ buffer: buffer, cache_root: cache_root,
396
+ paths: @argv.empty? ? nil : @argv
233
397
  )
234
398
  end
235
399
 
@@ -391,7 +555,10 @@ module Rigor
391
555
  end
392
556
 
393
557
  def inject_treat_all_as_inline_rbs(entries)
394
- filtered = entries.reject { |entry| rigor_rbs_inline_entry?(entry) }
558
+ # Single-homed with the WD2 auto-wire gate (`Configuration.rbs_inline_plugin_entry?`) so the flag and
559
+ # the default agree on what "already listed" means; a pre-existing entry is removed so this one's
560
+ # `require_magic_comment: false` wins unconditionally.
561
+ filtered = entries.reject { |entry| Configuration.rbs_inline_plugin_entry?(entry) }
395
562
  filtered + [{
396
563
  "gem" => "rigor-rbs-inline",
397
564
  "id" => "rbs-inline",
@@ -399,18 +566,6 @@ module Rigor
399
566
  }]
400
567
  end
401
568
 
402
- def rigor_rbs_inline_entry?(entry)
403
- case entry
404
- when String
405
- entry == "rigor-rbs-inline"
406
- when Hash
407
- string_keyed = entry.to_h { |k, v| [k.to_s, v] }
408
- string_keyed["gem"] == "rigor-rbs-inline" || string_keyed["id"] == "rbs-inline"
409
- else
410
- false
411
- end
412
- end
413
-
414
569
  def handle_clear_cache(cache_root)
415
570
  if File.directory?(cache_root)
416
571
  FileUtils.rm_rf(cache_root)
@@ -451,6 +606,42 @@ module Rigor
451
606
  @err.puts(" recursion-fixpoint-cap hits: #{counts[Inference::BudgetTrace::RECURSION_FIXPOINT_CAP]}")
452
607
  @err.puts(" block-writeback-cap hits: #{counts[Inference::BudgetTrace::BLOCK_WRITEBACK_CAP]}")
453
608
  write_budget_distributions
609
+ write_memo_profile(counts)
610
+ end
611
+
612
+ # Dumps the ADR-57 return-memo profile (WD0). Entries / hit rate / body evals / the non-store split, then
613
+ # the top signatures by body-eval count with their distinct-memo-key counts — the evidence for choosing
614
+ # between a finalization-aware taint gate and memo-key normalization. `--workers 0` for exact counts.
615
+ def write_memo_profile(counts)
616
+ bt = Inference::BudgetTrace
617
+ hits = counts[bt::MEMO_HITS]
618
+ misses = counts[bt::MEMO_MISSES]
619
+ consults = hits + misses
620
+ rate = consults.positive? ? format("%.1f%%", 100.0 * hits / consults) : "n/a"
621
+ @err.puts("")
622
+ @err.puts("Return-memo profile (RIGOR_BUDGET_TRACE; --workers 0 for an exact count)")
623
+ @err.puts(" infer entries: #{counts[bt::MEMO_ENTRIES]}")
624
+ @err.puts(" memo consults: #{consults} (hits #{hits} / misses #{misses}; hit rate #{rate})")
625
+ @err.puts(" body evals: #{counts[bt::MEMO_BODY_EVALS]}")
626
+ @err.puts(" non-stored: on-stack #{counts[bt::MEMO_REFUSE_ON_STACK]} " \
627
+ "unroll-in-flight #{counts[bt::MEMO_REFUSE_UNROLL]} " \
628
+ "consult-tainted #{counts[bt::MEMO_REFUSE_CONSULT_TAINTED]} " \
629
+ "transient-tainted #{counts[bt::MEMO_REFUSE_TRANSIENT]}")
630
+ write_memo_signature_table
631
+ end
632
+
633
+ # Top 15 signatures by body-eval (compute) count, each with its distinct-memo-key count — a signature
634
+ # whose distinct-key count dwarfs its evals is arg-granularity thrash (the key-normalization candidate).
635
+ def write_memo_signature_table
636
+ bt = Inference::BudgetTrace
637
+ evals = bt.distribution(bt::MEMO_BODY_EVAL_BY_SIGNATURE)
638
+ return if evals.empty?
639
+
640
+ keys = bt.distribution(bt::MEMO_DISTINCT_KEY_BY_SIGNATURE)
641
+ @err.puts(" top signatures by body-eval count (evals / distinct-keys / signature):")
642
+ evals.sort_by { |sig, n| [-n, sig] }.first(15).each do |sig, n|
643
+ @err.puts(" #{n.to_s.rjust(8)} #{keys.fetch(sig, 0).to_s.rjust(6)} #{sig}")
644
+ end
454
645
  end
455
646
 
456
647
  # Dumps the read-only size distributions (ADR-41 Slice 2a). These observe how large unions actually get, with no
@@ -488,7 +679,6 @@ module Rigor
488
679
  def load_check_dependencies
489
680
  require_relative "../analysis/runner"
490
681
  require_relative "../analysis/buffer_binding"
491
- require_relative "../analysis/baseline"
492
682
  require_relative "../cache/store"
493
683
  start_heap_trace_if_requested
494
684
  end
@@ -625,6 +815,7 @@ module Rigor
625
815
  def compute_coverage(runner, configuration, options)
626
816
  return nil unless options.fetch(:coverage)
627
817
 
818
+ require_relative "coverage_scan"
628
819
  files = @argv.empty? ? runner.analysis_file_set : runner.analysis_file_set(@argv)
629
820
  CoverageScan.precision_report(files: files, configuration: configuration)
630
821
  end
@@ -682,7 +873,7 @@ module Rigor
682
873
  return unless options.fetch(:ci_detect)
683
874
  return unless options.fetch(:format) == "text"
684
875
 
685
- platform = CLI::CiDetector.detect
876
+ platform = CiDetector.detect
686
877
  return if platform.nil?
687
878
 
688
879
  if platform.native_stdout?
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class CLI
5
+ # The one place a CLI command turns a loaded {Configuration} into an {Analysis::Result}.
6
+ #
7
+ # {CheckRunnerFactory} builds the runner; this module owns the *invocation* wrapped around it — resolving the
8
+ # analysed paths, running, and handing back both the runner (`rigor check`'s reporting tail still needs its cache
9
+ # store and stats) and the raw result. It exists so a non-`check` command — `rigor doctor`, `rigor skill describe
10
+ # --deep` — reaches a check result through the same code path `rigor check` walks, instead of re-deriving the
11
+ # plumbing (issue #148; ADR-73 § "Field-trial follow-ups" names the shared helper as the prerequisite).
12
+ #
13
+ # Nothing here is required at load time: {CheckRunnerFactory} pulls the inference engine, so both entry points
14
+ # require it lazily. A command that merely *mentions* this module therefore stays engine-free until it actually
15
+ # runs an analysis — the property ADR-87 WD4 protects for `rigor check`'s cache-hit fast path, and the property
16
+ # that keeps an un-flagged `rigor skill describe` from loading the engine at all.
17
+ module CheckInvocation
18
+ # A completed analysis. `runner` is exposed because `rigor check`'s tail reads its `cache_store` (eviction,
19
+ # `--cache-stats`) and feeds it to `--coverage`; `result` is the raw, pre-baseline-filter {Analysis::Result}.
20
+ Invocation = Data.define(:runner, :result)
21
+
22
+ # The outcome of {.attempt} — the best-effort variant. Exactly one of the two is non-nil, so a caller can never
23
+ # read "the check could not run" as "the check ran clean": `result` is nil precisely when `error` is set.
24
+ Outcome = Data.define(:result, :error) do
25
+ # @return [Boolean] true when an analysis actually completed.
26
+ def ran?
27
+ !result.nil?
28
+ end
29
+ end
30
+
31
+ # Options for a caller that wants a check *result* rather than a check *run*: no cache writes, no explain
32
+ # traces, sequential. `stats: true` because the RBS-environment signal (`stats.rbs_classes_total`) lives there.
33
+ # This is `rigor doctor`'s preset — a diagnostic command that must not churn the project's cache.
34
+ READ_ONLY_OPTIONS = { no_cache: true, explain: false, stats: true, workers: 0 }.freeze
35
+
36
+ # Options for an opt-in deep probe (`rigor skill describe --deep`). Unlike {READ_ONLY_OPTIONS} this uses the
37
+ # configured cache and worker count, i.e. it behaves exactly like `rigor check` — fast on a warm cache, and it
38
+ # WRITES `.rigor/cache`. That side effect is the whole reason the flag is opt-in.
39
+ DEEP_OPTIONS = { no_cache: false, explain: false, stats: true, workers: nil }.freeze
40
+
41
+ module_function
42
+
43
+ # Runs the analysis the way `rigor check` does and returns the runner + its raw result.
44
+ #
45
+ # @param configuration [Rigor::Configuration]
46
+ # @param options [Hash] at least `:no_cache`, `:explain`, `:stats`, `:workers` (see {CheckRunnerFactory.build}).
47
+ # @param paths [Array<String>, nil] analysed paths; nil falls back to the configuration's `paths:`.
48
+ # @param buffer [Rigor::Analysis::BufferBinding, nil]
49
+ # @param cache_root [String, nil] nil falls back to the configuration's cache path.
50
+ # @return [Invocation]
51
+ def run(configuration:, options:, paths: nil, buffer: nil, cache_root: nil)
52
+ require_relative "check_runner_factory"
53
+ runner = CheckRunnerFactory.build(
54
+ configuration: configuration,
55
+ options: options,
56
+ buffer: buffer,
57
+ cache_root: cache_root || configuration.cache_path
58
+ )
59
+ Invocation.new(runner: runner, result: runner.run(paths || configuration.paths))
60
+ end
61
+
62
+ # Best-effort variant for a *routing* caller: one that wants to sharpen a recommendation with check evidence and
63
+ # has a perfectly good answer when there is none. Loads the configuration itself and converts every way the run
64
+ # can fail outright — an unreadable / invalid config, an unloadable plugin, an engine error — into an {Outcome}
65
+ # carrying a short human-readable `error`. It never raises and never fabricates a clean result.
66
+ #
67
+ # Deliberately broad: the caller's contract is "degrade to the presence-only answer, whatever went wrong". A
68
+ # routing hint is outside the false-positive envelope, but a crash in it is not — `describe` must stay a command
69
+ # an agent can run freely.
70
+ #
71
+ # @param config_path [String, nil] path to the config file; nil uses {Configuration.discover}.
72
+ # @param options [Hash] runner options (defaults to {DEEP_OPTIONS}).
73
+ # @param paths [Array<String>, nil]
74
+ # @return [Outcome]
75
+ def attempt(config_path: nil, options: DEEP_OPTIONS, paths: nil)
76
+ require_relative "../configuration"
77
+ configuration = Configuration.load(config_path)
78
+ Outcome.new(result: run(configuration: configuration, options: options, paths: paths).result, error: nil)
79
+ rescue StandardError, LoadError => e
80
+ Outcome.new(result: nil, error: "#{e.class}: #{e.message.to_s.lines.first.to_s.strip}")
81
+ end
82
+ end
83
+ end
84
+ end
@@ -14,6 +14,7 @@ require_relative "../inference/parameter_inference_collector"
14
14
  require_relative "../protection/mutation_scanner"
15
15
  require_relative "../protection/test_suite_oracle"
16
16
  require_relative "../language_server/project_context"
17
+ require_relative "../runtime/jit"
17
18
  require_relative "../scope"
18
19
  require_relative "coverage_report"
19
20
  require_relative "coverage_renderer"
@@ -53,7 +54,11 @@ module Rigor
53
54
  DEFAULT_TEST_COMMAND = %w[bundle exec rake].freeze
54
55
 
55
56
  # @return [Integer] CLI exit status.
56
- def run
57
+ def run # rubocop:disable Metrics/AbcSize
58
+ # Arm deferred YJIT enablement (Runtime::Jit): a scan long enough to
59
+ # amortize JIT compile cost enables mid-flight; a short one finishes
60
+ # first and never pays it. Same seam as `rigor check`.
61
+ Runtime::Jit.enable_after(Runtime::Jit.deadline_seconds)
57
62
  options = parse_options
58
63
  return mutation_misuse_error if options[:mutation] && !options[:protection]
59
64
  return with_tests_misuse_error if options[:with_tests] && !options[:mutation]
@@ -20,9 +20,6 @@ module Rigor
20
20
  #
21
21
  # `<name>` resolves a category-qualified path (`handbook/03-narrowing`), a prefixed basename (`03-narrowing`), or a
22
22
  # short name (`narrowing`, when it is unique across categories).
23
- #
24
- # The pre-v0.3.0 verb spellings `rigor docs list` / `rigor docs path <name>` still work but emit a stderr
25
- # deprecation notice; they are removed in v0.3.0 (see docs/ROADMAP.md § "Scheduled CLI deprecations").
26
23
  class DocsCommand < Command
27
24
  USAGE = <<~USAGE
28
25
  Usage: rigor docs [<name>] [--path <name>] [--list [<category>]]
@@ -46,10 +43,6 @@ module Rigor
46
43
  rigor docs editor-integration
47
44
  rigor docs --path 17-driving-improvement
48
45
  rigor docs --list handbook
49
-
50
- Deprecated (removed in v0.3.0) — use the flags above:
51
- rigor docs list -> rigor docs --list
52
- rigor docs path <name> -> rigor docs --path <name>
53
46
  USAGE
54
47
 
55
48
  # The bundled docs live at `<gem_root>/docs/`. From `lib/rigor/cli/docs_command.rb` that is three directories up.
@@ -58,18 +51,8 @@ module Rigor
58
51
  HANDBOOK_ROOT = File.join(DOCS_ROOT, "handbook")
59
52
  LLMS_INDEX = File.join(DOCS_ROOT, "llms.txt")
60
53
 
61
- # The verb subcommands the flags superseded keep working with a stderr deprecation notice until this version drops
62
- # them. Each maps to the canonical advice printed and the flag it rewrites to.
63
- LEGACY_VERB_REMOVAL = "v0.3.0"
64
- LEGACY_VERBS = {
65
- "list" => { old: "list", advice: "--list", flag: "--list" },
66
- "path" => { old: "path <name>", advice: "--path <name>", flag: "--path" }
67
- }.freeze
68
-
69
54
  # @return [Integer] CLI exit status.
70
55
  def run
71
- rewrite_legacy_verb!
72
-
73
56
  case @argv.first
74
57
  when nil
75
58
  run_index
@@ -92,19 +75,6 @@ module Rigor
92
75
 
93
76
  private
94
77
 
95
- # Translate a deprecated verb spelling into its flag form, warning once on stderr, so the dispatch above only
96
- # handles canonical forms.
97
- def rewrite_legacy_verb!
98
- spec = LEGACY_VERBS[@argv.first]
99
- return unless spec
100
-
101
- @err.puts(
102
- "rigor docs: `#{spec.fetch(:old)}` is deprecated and will be removed in " \
103
- "#{LEGACY_VERB_REMOVAL}; use `rigor docs #{spec.fetch(:advice)}` instead."
104
- )
105
- @argv[0] = spec.fetch(:flag)
106
- end
107
-
108
78
  def run_index
109
79
  if File.file?(LLMS_INDEX)
110
80
  @out.write(File.read(LLMS_INDEX))