rigortype 0.2.9 → 0.3.0

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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/docs/handbook/03-narrowing.md +2 -1
  4. data/docs/handbook/04-tuples-and-shapes.md +7 -3
  5. data/docs/handbook/06-classes.md +18 -9
  6. data/docs/handbook/08-understanding-errors.md +1 -0
  7. data/docs/handbook/11-sig-gen.md +11 -1
  8. data/docs/handbook/appendix-mypy.md +4 -3
  9. data/docs/install.md +14 -6
  10. data/docs/manual/01-installation.md +98 -4
  11. data/docs/manual/02-cli-reference.md +49 -12
  12. data/docs/manual/03-configuration.md +34 -2
  13. data/docs/manual/04-diagnostics.md +23 -0
  14. data/docs/manual/07-plugins.md +4 -4
  15. data/docs/manual/09-editor-integration.md +4 -4
  16. data/docs/manual/11-ci.md +51 -0
  17. data/docs/manual/12-caching.md +65 -0
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
  20. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
  21. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
  22. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  23. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
  24. data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
  25. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  26. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
  27. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
  28. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
  29. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
  30. data/lib/rigor/analysis/check_rules.rb +557 -95
  31. data/lib/rigor/analysis/dependency_recorder.rb +93 -9
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/incremental_session.rb +456 -51
  34. data/lib/rigor/analysis/path_expansion.rb +42 -0
  35. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
  36. data/lib/rigor/analysis/rule_catalog.rb +226 -1
  37. data/lib/rigor/analysis/run_cache_key.rb +68 -0
  38. data/lib/rigor/analysis/run_cache_probe.rb +72 -0
  39. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
  40. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
  41. data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
  42. data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
  43. data/lib/rigor/analysis/runner.rb +343 -68
  44. data/lib/rigor/analysis/severity_stamp.rb +43 -0
  45. data/lib/rigor/analysis/worker_session.rb +30 -2
  46. data/lib/rigor/bleeding_edge.rb +37 -8
  47. data/lib/rigor/builtins/regex_refinement.rb +36 -1
  48. data/lib/rigor/cache/descriptor.rb +88 -28
  49. data/lib/rigor/cache/file_digest.rb +158 -0
  50. data/lib/rigor/cache/incremental_snapshot.rb +41 -5
  51. data/lib/rigor/cache/rbs_descriptor.rb +56 -10
  52. data/lib/rigor/cache/store.rb +33 -3
  53. data/lib/rigor/ci_detector.rb +90 -0
  54. data/lib/rigor/cli/annotate_command.rb +15 -9
  55. data/lib/rigor/cli/baseline_command.rb +4 -1
  56. data/lib/rigor/cli/check_command.rb +212 -27
  57. data/lib/rigor/cli/coverage_command.rb +6 -1
  58. data/lib/rigor/cli/docs_command.rb +0 -30
  59. data/lib/rigor/cli/doctor_command.rb +163 -23
  60. data/lib/rigor/cli/lsp_command.rb +5 -0
  61. data/lib/rigor/cli/mcp_command.rb +5 -0
  62. data/lib/rigor/cli/plugins_command.rb +36 -6
  63. data/lib/rigor/cli/plugins_renderer.rb +44 -12
  64. data/lib/rigor/cli/probe_environment.rb +85 -0
  65. data/lib/rigor/cli/sig_gen_command.rb +36 -9
  66. data/lib/rigor/cli/skill_command.rb +1 -32
  67. data/lib/rigor/cli/trace_command.rb +5 -6
  68. data/lib/rigor/cli/type_of_command.rb +9 -8
  69. data/lib/rigor/cli/type_scan_command.rb +8 -8
  70. data/lib/rigor/cli.rb +15 -4
  71. data/lib/rigor/config_audit.rb +48 -2
  72. data/lib/rigor/configuration/severity_profile.rb +31 -0
  73. data/lib/rigor/configuration.rb +156 -8
  74. data/lib/rigor/environment/default_libraries.rb +35 -0
  75. data/lib/rigor/environment/rbs_loader.rb +164 -5
  76. data/lib/rigor/environment.rb +19 -27
  77. data/lib/rigor/inference/budget_trace.rb +77 -2
  78. data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
  79. data/lib/rigor/inference/def_handle.rb +23 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +90 -0
  81. data/lib/rigor/inference/def_return_typer.rb +2 -1
  82. data/lib/rigor/inference/expression_typer.rb +361 -113
  83. data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
  84. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
  85. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
  86. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
  87. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
  88. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
  89. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
  90. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
  91. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
  92. data/lib/rigor/inference/method_dispatcher.rb +57 -1
  93. data/lib/rigor/inference/mutation_widening.rb +42 -8
  94. data/lib/rigor/inference/narrowing.rb +127 -17
  95. data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
  96. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  97. data/lib/rigor/inference/rbs_type_translator.rb +8 -1
  98. data/lib/rigor/inference/scope_indexer.rb +451 -36
  99. data/lib/rigor/inference/statement_evaluator.rb +64 -15
  100. data/lib/rigor/inference/struct_fold_safety.rb +48 -11
  101. data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
  102. data/lib/rigor/inference/void_origin.rb +25 -0
  103. data/lib/rigor/inference/void_tail_summary.rb +220 -0
  104. data/lib/rigor/language_server/completion_provider.rb +2 -1
  105. data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
  106. data/lib/rigor/language_server/folding_range_provider.rb +2 -1
  107. data/lib/rigor/language_server/selection_range_provider.rb +2 -1
  108. data/lib/rigor/language_server/signature_help_provider.rb +2 -1
  109. data/lib/rigor/plugin/base.rb +9 -22
  110. data/lib/rigor/plugin/inflector.rb +12 -3
  111. data/lib/rigor/plugin/io_boundary.rb +5 -1
  112. data/lib/rigor/plugin/isolation.rb +81 -11
  113. data/lib/rigor/plugin/load_error.rb +10 -1
  114. data/lib/rigor/plugin/loader.rb +96 -14
  115. data/lib/rigor/plugin/node_rule_walk.rb +5 -3
  116. data/lib/rigor/plugin/registry.rb +32 -23
  117. data/lib/rigor/plugin.rb +26 -0
  118. data/lib/rigor/protection/mutator.rb +3 -2
  119. data/lib/rigor/reflection.rb +64 -0
  120. data/lib/rigor/runtime/jit.rb +128 -0
  121. data/lib/rigor/scope/discovery_index.rb +12 -2
  122. data/lib/rigor/scope.rb +122 -28
  123. data/lib/rigor/sig_gen/classification.rb +5 -1
  124. data/lib/rigor/sig_gen/generator.rb +38 -6
  125. data/lib/rigor/sig_gen/observation_collector.rb +4 -3
  126. data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
  127. data/lib/rigor/sig_gen/renderer.rb +10 -0
  128. data/lib/rigor/sig_gen/write_result.rb +9 -4
  129. data/lib/rigor/sig_gen/writer.rb +24 -2
  130. data/lib/rigor/source/node_children.rb +116 -0
  131. data/lib/rigor/source/node_locator.rb +3 -1
  132. data/lib/rigor/source/node_walker.rb +4 -2
  133. data/lib/rigor/source.rb +1 -0
  134. data/lib/rigor/type/difference.rb +28 -24
  135. data/lib/rigor/type/hash_shape.rb +34 -10
  136. data/lib/rigor/version.rb +1 -1
  137. data/lib/rigortype.rb +24 -0
  138. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
  139. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
  140. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
  141. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
  142. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
  143. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
  144. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
  145. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
  146. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
  147. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
  148. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
  149. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
  150. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
  151. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
  152. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
  153. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
  154. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
  155. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
  156. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
  157. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
  158. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
  159. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
  160. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
  161. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
  162. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
  164. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
  166. data/sig/prism_node_children.rbs +9 -0
  167. data/sig/rigor/inference.rbs +7 -0
  168. data/sig/rigor/plugin/base.rbs +2 -3
  169. data/sig/rigor/reflection.rbs +1 -0
  170. data/sig/rigor/scope.rbs +12 -1
  171. data/skills/rigor-ci-setup/SKILL.md +10 -0
  172. data/skills/rigor-editor-setup/SKILL.md +11 -0
  173. data/skills/rigor-next-steps/SKILL.md +23 -2
  174. data/skills/rigor-plugin-author/SKILL.md +3 -3
  175. data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
  176. data/skills/rigor-plugin-review/SKILL.md +2 -2
  177. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
  178. data/skills/rigor-project-init/SKILL.md +29 -3
  179. data/skills/rigor-project-init/references/01-detect.md +1 -1
  180. data/skills/rigor-project-init/references/02-configure.md +39 -5
  181. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
  182. data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
  183. metadata +42 -24
  184. data/lib/rigor/cli/ci_detector.rb +0 -89
@@ -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,18 +34,34 @@ 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
 
@@ -66,23 +87,113 @@ module Rigor
66
87
 
67
88
  private
68
89
 
90
+ # ADR-87 WD4 — attempt the boot-slimming run-cache hit. Returns the cached {Analysis::Result} (severity
91
+ # profile applied, no stats — matching a cache-served `Runner#run`) on a hit, or nil to fall through to
92
+ # the full engine path. Only an ordinary sequential check whose result IS cache-served the same way is
93
+ # eligible; every other mode (below) declines so the full path handles it unchanged.
94
+ def try_run_cache_hit(configuration, options, buffer, cache_root)
95
+ return nil unless run_cache_hit_eligible?(configuration, options, buffer)
96
+
97
+ require_relative "../analysis/run_cache_probe"
98
+ Analysis::RunCacheProbe.new(
99
+ configuration: configuration, cache_root: cache_root, explain: options.fetch(:explain)
100
+ ).serve(@argv.empty? ? configuration.paths : @argv)
101
+ end
102
+
103
+ # The probe is eligible only for a run the ADR-45 result cache actually serves: a writable cache (not
104
+ # `--no-cache`), no editor buffer, sequential (pool runs are not result-cacheable), and no mode needing
105
+ # the engine (`--coverage` / `--incremental` / `--verify-incremental`) or the runner's own reporting
106
+ # (`--cache-stats`, the `RIGOR_*_TRACE` dev probes). A wrongly-permitted run still MISSES (the entry was
107
+ # never written its way) and falls through — the gate is an optimisation, never a soundness boundary.
108
+ def run_cache_hit_eligible?(configuration, options, buffer)
109
+ buffer.nil? &&
110
+ !options.fetch(:no_cache) && !options.fetch(:coverage) && !options.fetch(:cache_stats) &&
111
+ !options.fetch(:incremental) && !options.fetch(:verify_incremental) &&
112
+ !pool_workers_configured?(options, configuration) &&
113
+ ENV["RIGOR_BUDGET_TRACE"].to_s.empty? && ENV["RIGOR_HEAP_TRACE"].to_s.empty?
114
+ end
115
+
116
+ # Any signal that per-file analysis would run in a worker pool (CLI `--workers`, the env override, or the
117
+ # config default). Deliberately over-declines (a false positive only forgoes the fast lane); mirrors
118
+ # {CheckRunnerFactory.resolve_workers} without requiring it (that pulls in the engine).
119
+ def pool_workers_configured?(options, configuration)
120
+ cli = options[:workers]
121
+ return Integer(cli).positive? if cli
122
+
123
+ env = ENV.fetch("RIGOR_RACTOR_WORKERS", nil)
124
+ return Integer(env).positive? if env && !env.empty?
125
+
126
+ configuration.parallel_workers.positive?
127
+ rescue ArgumentError
128
+ false
129
+ end
130
+
131
+ # ADR-87 WD4 — the engine-free tail for a cache-served hit: baseline filter + output + exit code,
132
+ # mirroring the full path's tail but without stats (a hit collected none), trace appendices (they read
133
+ # engine counters), or eviction (deferred to the next miss). `raw_result` is pre-baseline so the strict
134
+ # gate reads the same diagnostics the full path would.
135
+ def finalize_cache_hit(raw_result, configuration, options, config_warnings)
136
+ result = apply_baseline_filter(raw_result, configuration, options)
137
+ write_result(result, options.fetch(:format), config_warnings: config_warnings)
138
+ emit_ci_detected_output(result, options)
139
+
140
+ exit_code = result.success? ? 0 : 1
141
+ exit_code = 1 if baseline_strict_violation?(raw_result.diagnostics, configuration, options)
142
+ exit_code
143
+ end
144
+
145
+ # ADR-67 WD6c — `parameter_inference:` and the ADR-46 incremental modes are mutually exclusive in slice 1.
146
+ # The parameter table introduces cross-file edges (a caller's argument types drive a callee's body
147
+ # diagnostics) that the per-file dependency recorder does not yet carry, so a cached file whose parameter
148
+ # seeds changed would serve a stale result. Refuse with a message naming the ADR-46 gap rather than
149
+ # silently producing unsound incremental diagnostics; the edge wiring is the named follow-up. Returns the
150
+ # usage exit code on a conflict, or nil to proceed. `--verify-incremental` (the acceptance gate that runs
151
+ # incremental analysis) is caught here too.
152
+ def parameter_inference_incremental_conflict(configuration, options)
153
+ return nil unless configuration.parameter_inference
154
+ return nil unless options.fetch(:incremental) || options.fetch(:verify_incremental)
155
+
156
+ flag = options.fetch(:incremental) ? "--incremental" : "--verify-incremental"
157
+ @err.puts("rigor: parameter_inference: cannot combine with #{flag} — the call-site parameter table " \
158
+ "introduces cross-file caller→callee edges the ADR-46 incremental dependency graph does not " \
159
+ "yet record, so a cached file could serve a stale diagnostic. Run a full check (drop " \
160
+ "#{flag}), or disable parameter_inference: for this run.")
161
+ CLI::EXIT_USAGE
162
+ end
163
+
69
164
  # ADR-46 — the two incremental-analysis check modes both fully handle the run and return an exit code (so `run`
70
165
  # short-circuits); returns nil for an ordinary check.
71
166
  def dispatch_special_check_mode(configuration, options, cache_root)
72
- return run_verify_incremental(configuration) if options.fetch(:verify_incremental)
167
+ return run_verify_incremental(configuration, options, cache_root) if options.fetch(:verify_incremental)
73
168
  return run_incremental_check(configuration, options, cache_root) if options.fetch(:incremental)
74
169
 
75
170
  nil
76
171
  end
77
172
 
173
+ # ADR-85 WD1 — the persistent cache the incremental session threads into each internal Runner so plugin
174
+ # `#prepare` producers (the ADR-9/#74/ADR-60 WD3 record-and-validate caches) and the RBS environment serve
175
+ # from disk across processes / rechecks rather than recomputing every invocation. Honors `--no-cache` (nil
176
+ # = no store), mirroring {CheckRunnerFactory}. The whole-run ADR-45 result cache stays inert on these runs
177
+ # — `Runner#run_result_cacheable?` excludes `record_dependencies` / `analyze_only`.
178
+ def incremental_cache_store(configuration, options, cache_root)
179
+ return nil if options.fetch(:no_cache)
180
+
181
+ Cache::Store.new(root: cache_root, max_bytes: configuration.cache_max_bytes)
182
+ end
183
+
78
184
  # ADR-46 — the incremental-analysis acceptance gate. Runs a baseline analysis (recording cross-file dependencies),
79
185
  # then re-analyzes a representative subset of files and serves the rest from the per-file cache (the body tier),
80
186
  # and asserts the merged diagnostics are byte-identical to a full `--no-cache` analysis. A mismatch means the
81
187
  # incremental machinery would serve a stale — manufactured — diagnostic, the soundness failure this gate exists to
82
188
  # catch. Prints a one-line PASS (exit 0) or the differing diagnostics (exit 1).
83
- def run_verify_incremental(configuration)
189
+ def run_verify_incremental(configuration, options, cache_root)
84
190
  paths = @argv.empty? ? nil : @argv
85
- session = Analysis::IncrementalSession.new(configuration: configuration, paths: paths)
191
+ # ADR-85 WD1 thread the store so the gate also asserts the cache-served producer path stays
192
+ # byte-identical to the uncached full run (`verify_full_diagnostics` builds a nil-store runner).
193
+ session = Analysis::IncrementalSession.new(
194
+ configuration: configuration, paths: paths,
195
+ cache_store: incremental_cache_store(configuration, options, cache_root)
196
+ )
86
197
  session.baseline
87
198
  analyzed = session.analyzed_files
88
199
 
@@ -101,18 +212,30 @@ module Rigor
101
212
  # updated snapshot for the next invocation. Diagnostics are identical to a full run (the `--verify-incremental`
102
213
  # gate enforces this); the win is skipping per-file inference for unchanged files.
103
214
  def run_incremental_check(configuration, options, cache_root)
215
+ require_relative "check_runner_factory"
104
216
  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
217
  fingerprint = Cache::IncrementalSnapshot.fingerprint(
108
218
  configuration: configuration, roots: paths || configuration.paths
109
219
  )
110
220
  snapshot = Cache::IncrementalSnapshot.new(root: cache_root)
111
- session = Analysis::IncrementalSession.new(configuration: configuration, paths: paths)
221
+ store = incremental_cache_store(configuration, options, cache_root)
222
+ session = Analysis::IncrementalSession.new(
223
+ configuration: configuration, paths: paths,
224
+ cache_store: store,
225
+ # ADR-46 — thread the same worker-count precedence the standard `check` path uses
226
+ # (CLI `--workers` > `RIGOR_RACTOR_WORKERS` > `parallel.workers:` > 0) so the recheck's closure
227
+ # re-analysis parallelises; the fork pool marshals dependency records back so the graph is sound.
228
+ workers: CheckRunnerFactory.resolve_workers(options, configuration)
229
+ )
112
230
 
113
231
  diagnostics, warm = session.run_incremental(snapshot: snapshot, fingerprint: fingerprint)
232
+ # The banner's file count comes from the session's analyzed set (cold analyses all; a warm recheck's
233
+ # `@analyzed` advances to the current file set), so a dedicated probe Runner + `Dir.glob` is no longer
234
+ # built just to size the banner (recon §3 — the analysis tree was expanded three times per recheck).
114
235
  @err.puts("rigor: --incremental #{warm ? 'warm — reused cached diagnostics' : 'cold — full analysis'} " \
115
- "(#{files.size} files)")
236
+ "(#{session.analyzed_files.size} files)")
237
+ emit_incremental_fact_surface_notes(session)
238
+ write_incremental_cache_stats(session, cache_root, store) if options.fetch(:cache_stats)
116
239
 
117
240
  result = apply_baseline_filter(Analysis::Result.new(diagnostics: diagnostics, stats: nil), configuration,
118
241
  options)
@@ -120,6 +243,40 @@ module Rigor
120
243
  result.success? ? 0 : 1
121
244
  end
122
245
 
246
+ # ADR-88 WD1 — a one-line stderr note when the plugin fact surface (an ADR-9 fact, an ADR-60 producer
247
+ # value, or an `incremental_state_fingerprint` hook) forced a full run: either it CHANGED since the
248
+ # snapshot (a Sorbet sig edit, a schema change) or a contributing plugin declares NO fingerprint surface
249
+ # (opaque — a full run every invocation). Both are the sound direction; the note explains why a recheck
250
+ # was not warm.
251
+ def emit_incremental_fact_surface_notes(session)
252
+ opaque = session.opaque_plugin_ids
253
+ unless opaque.empty?
254
+ @err.puts("rigor: --incremental plugin#{'s' unless opaque.size == 1} #{opaque.sort.join(', ')} " \
255
+ "contribute call-site types with no incremental fingerprint surface; the snapshot cannot " \
256
+ "be reused (full analysis every run).")
257
+ end
258
+ return unless session.fact_surface_invalidated? && opaque.empty?
259
+
260
+ @err.puts("rigor: --incremental plugin fact surface changed since the snapshot; ran a full analysis.")
261
+ end
262
+
263
+ # ADR-88 WD1 — `--incremental --cache-stats` surfaces the on-disk cache inventory (as a plain `check`
264
+ # does) plus the fact-surface status line, so an operator can see whether a full run was fact-surface
265
+ # driven. The plain `check` cache-stats path is bypassed by the incremental short-circuit, so it is
266
+ # emitted here.
267
+ def write_incremental_cache_stats(session, cache_root, store)
268
+ write_cache_stats(cache_root, store)
269
+ status =
270
+ if !session.opaque_plugin_ids.empty?
271
+ "opaque (#{session.opaque_plugin_ids.sort.join(', ')})"
272
+ elsif session.fact_surface_invalidated?
273
+ "changed — snapshot invalidated"
274
+ else
275
+ "unchanged"
276
+ end
277
+ @out.puts(" plugin fact surface: #{status}")
278
+ end
279
+
123
280
  def verify_full_diagnostics(configuration, paths)
124
281
  runner = Analysis::Runner.new(configuration: configuration, cache_store: nil)
125
282
  (paths ? runner.run(paths) : runner.run).diagnostics
@@ -227,6 +384,7 @@ module Rigor
227
384
  end
228
385
 
229
386
  def build_check_runner(configuration:, options:, buffer:, cache_root:)
387
+ require_relative "check_runner_factory"
230
388
  CheckRunnerFactory.build(
231
389
  configuration: configuration, options: options,
232
390
  buffer: buffer, cache_root: cache_root
@@ -391,7 +549,10 @@ module Rigor
391
549
  end
392
550
 
393
551
  def inject_treat_all_as_inline_rbs(entries)
394
- filtered = entries.reject { |entry| rigor_rbs_inline_entry?(entry) }
552
+ # Single-homed with the WD2 auto-wire gate (`Configuration.rbs_inline_plugin_entry?`) so the flag and
553
+ # the default agree on what "already listed" means; a pre-existing entry is removed so this one's
554
+ # `require_magic_comment: false` wins unconditionally.
555
+ filtered = entries.reject { |entry| Configuration.rbs_inline_plugin_entry?(entry) }
395
556
  filtered + [{
396
557
  "gem" => "rigor-rbs-inline",
397
558
  "id" => "rbs-inline",
@@ -399,18 +560,6 @@ module Rigor
399
560
  }]
400
561
  end
401
562
 
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
563
  def handle_clear_cache(cache_root)
415
564
  if File.directory?(cache_root)
416
565
  FileUtils.rm_rf(cache_root)
@@ -451,6 +600,42 @@ module Rigor
451
600
  @err.puts(" recursion-fixpoint-cap hits: #{counts[Inference::BudgetTrace::RECURSION_FIXPOINT_CAP]}")
452
601
  @err.puts(" block-writeback-cap hits: #{counts[Inference::BudgetTrace::BLOCK_WRITEBACK_CAP]}")
453
602
  write_budget_distributions
603
+ write_memo_profile(counts)
604
+ end
605
+
606
+ # Dumps the ADR-57 return-memo profile (WD0). Entries / hit rate / body evals / the non-store split, then
607
+ # the top signatures by body-eval count with their distinct-memo-key counts — the evidence for choosing
608
+ # between a finalization-aware taint gate and memo-key normalization. `--workers 0` for exact counts.
609
+ def write_memo_profile(counts)
610
+ bt = Inference::BudgetTrace
611
+ hits = counts[bt::MEMO_HITS]
612
+ misses = counts[bt::MEMO_MISSES]
613
+ consults = hits + misses
614
+ rate = consults.positive? ? format("%.1f%%", 100.0 * hits / consults) : "n/a"
615
+ @err.puts("")
616
+ @err.puts("Return-memo profile (RIGOR_BUDGET_TRACE; --workers 0 for an exact count)")
617
+ @err.puts(" infer entries: #{counts[bt::MEMO_ENTRIES]}")
618
+ @err.puts(" memo consults: #{consults} (hits #{hits} / misses #{misses}; hit rate #{rate})")
619
+ @err.puts(" body evals: #{counts[bt::MEMO_BODY_EVALS]}")
620
+ @err.puts(" non-stored: on-stack #{counts[bt::MEMO_REFUSE_ON_STACK]} " \
621
+ "unroll-in-flight #{counts[bt::MEMO_REFUSE_UNROLL]} " \
622
+ "consult-tainted #{counts[bt::MEMO_REFUSE_CONSULT_TAINTED]} " \
623
+ "transient-tainted #{counts[bt::MEMO_REFUSE_TRANSIENT]}")
624
+ write_memo_signature_table
625
+ end
626
+
627
+ # Top 15 signatures by body-eval (compute) count, each with its distinct-memo-key count — a signature
628
+ # whose distinct-key count dwarfs its evals is arg-granularity thrash (the key-normalization candidate).
629
+ def write_memo_signature_table
630
+ bt = Inference::BudgetTrace
631
+ evals = bt.distribution(bt::MEMO_BODY_EVAL_BY_SIGNATURE)
632
+ return if evals.empty?
633
+
634
+ keys = bt.distribution(bt::MEMO_DISTINCT_KEY_BY_SIGNATURE)
635
+ @err.puts(" top signatures by body-eval count (evals / distinct-keys / signature):")
636
+ evals.sort_by { |sig, n| [-n, sig] }.first(15).each do |sig, n|
637
+ @err.puts(" #{n.to_s.rjust(8)} #{keys.fetch(sig, 0).to_s.rjust(6)} #{sig}")
638
+ end
454
639
  end
455
640
 
456
641
  # Dumps the read-only size distributions (ADR-41 Slice 2a). These observe how large unions actually get, with no
@@ -488,7 +673,6 @@ module Rigor
488
673
  def load_check_dependencies
489
674
  require_relative "../analysis/runner"
490
675
  require_relative "../analysis/buffer_binding"
491
- require_relative "../analysis/baseline"
492
676
  require_relative "../cache/store"
493
677
  start_heap_trace_if_requested
494
678
  end
@@ -625,6 +809,7 @@ module Rigor
625
809
  def compute_coverage(runner, configuration, options)
626
810
  return nil unless options.fetch(:coverage)
627
811
 
812
+ require_relative "coverage_scan"
628
813
  files = @argv.empty? ? runner.analysis_file_set : runner.analysis_file_set(@argv)
629
814
  CoverageScan.precision_report(files: files, configuration: configuration)
630
815
  end
@@ -682,7 +867,7 @@ module Rigor
682
867
  return unless options.fetch(:ci_detect)
683
868
  return unless options.fetch(:format) == "text"
684
869
 
685
- platform = CLI::CiDetector.detect
870
+ platform = CiDetector.detect
686
871
  return if platform.nil?
687
872
 
688
873
  if platform.native_stdout?
@@ -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))