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
@@ -3,6 +3,7 @@
3
3
  require "yaml"
4
4
 
5
5
  require_relative "bleeding_edge"
6
+ require_relative "ci_detector"
6
7
  require_relative "configuration/dependencies"
7
8
  require_relative "configuration/severity_profile"
8
9
 
@@ -55,6 +56,13 @@ module Rigor
55
56
  # filtering — the path must be named here (WD2 (b) of ADR-22).
56
57
  "baseline" => nil,
57
58
  "fold_platform_specific_paths" => false,
59
+ # ADR-67 WD6a — opt-in call-site parameter type inference on the `check` walk. When `true`, `rigor
60
+ # check` runs the {Inference::ParameterInferenceCollector} as a one-round pre-pass into the discovery
61
+ # seed, so an undeclared `def` / `initialize` / setter parameter is typed to the union of its resolved
62
+ # call-site argument types (precision-additive only — the in-body negative rules decline on the inferred
63
+ # lower bound, WD6b). `false` (the default, resolved off by every severity profile) keeps the table empty
64
+ # and the run byte-identical to today. Mutually exclusive with `--incremental` (WD6c).
65
+ "parameter_inference" => false,
58
66
  "cache" => {
59
67
  "path" => ".rigor/cache",
60
68
  # LRU eviction cap in bytes (ADR-54 WD3). The least-recently-used entries are removed at the end of a
@@ -63,7 +71,18 @@ module Rigor
63
71
  # would otherwise ever delete; a full active per-project set is ~2 MB, so the cap never touches live
64
72
  # entries. Set explicitly to `null` to disable eviction (pre-WD3 behaviour: the cache grows until
65
73
  # `--clear-cache`).
66
- "max_bytes" => 268_435_456
74
+ "max_bytes" => 268_435_456,
75
+ # ADR-87 WD1 — file-freshness validation strategy. `"stat"` validates a recorded file
76
+ # dependency by stat-ing it first and re-hashing only when the `(size, mtime_ns, ctime_ns, inode)`
77
+ # tuple moved (or the racy window fires), so an unchanged monorepo hashes ~0 bytes on a warm run.
78
+ # `"digest"` restores the pre-ADR-87 behaviour of SHA-256'ing every recorded file on every run — the
79
+ # per-run escape hatch is the `RIGOR_STRICT_VALIDATION=1` env var, which wins over this setting.
80
+ # `"auto"` (default, #190) resolves per environment: `"digest"` when {CiDetector} recognises a CI
81
+ # provider — a fresh checkout regenerates every stat tuple, so the stat tier can never short-circuit
82
+ # there and stat-signature glob slots would recompute on every run — and `"stat"` everywhere else.
83
+ # A self-hosted runner with a persistent workspace opts back into the stat floor with an explicit
84
+ # `"stat"` (or `RIGOR_CI_DETECT=0`).
85
+ "validation" => "auto"
67
86
  },
68
87
  "plugins_io" => {
69
88
  "network" => "disabled",
@@ -143,9 +162,45 @@ module Rigor
143
162
  PATH_KEYS = %w[paths signature_paths pre_eval].freeze
144
163
  private_constant :PATH_KEYS
145
164
 
165
+ # Top-level keys this implementation DECLARES but never reads — see `docs/internal-spec/config.md`
166
+ # § "Reserved namespaces" and ADR-99.
167
+ #
168
+ # A sibling implementation (`rigor-rs`, which vendors our schema rather than keeping its own)
169
+ # groups keys for concepts we do not have under its own namespace, so one `.rigor.yml` feeds both.
170
+ # Such a key answers to the SCHEMA TIER ONLY: it is type-checked where it is written, and here it
171
+ # is never read, never validated, never coerced, and never an error however invalid its value.
172
+ #
173
+ # That is already the emergent behaviour — `#initialize` fetches each key it owns and never
174
+ # enumerates the rest — so this constant does not gate the runtime. It exists to make the
175
+ # reservation FINDABLE by the next person adding config validation (#166 is exactly that), and to
176
+ # give the schema-parity spec something to key on: a reserved namespace is by definition absent
177
+ # from DEFAULTS, so the DEFAULTS-driven schema gate can never see it.
178
+ RESERVED_NAMESPACES = %w[rigor_rs].freeze
179
+
180
+ # Every top-level key a conforming `.rigor.yml` may carry: the keys this implementation owns, plus
181
+ # `includes:`, plus the namespaces reserved for another implementation. Anything else is recorded in
182
+ # {#unknown_keys} and warned about by {ConfigAudit}, and is also the did-you-mean dictionary for a
183
+ # near-miss.
184
+ #
185
+ # `includes:` is a load-time directive — {load_with_includes} `delete`s it while merging, so it never
186
+ # reaches `#initialize` and could not be seen as unknown regardless. It is listed because a user
187
+ # legitimately writes it, so a typo like `include:` must be able to suggest it.
188
+ KNOWN_KEYS = (DEFAULTS.keys + %w[includes] + RESERVED_NAMESPACES).freeze
189
+
190
+ # Top-level keys the loaded config carried that this implementation does not own — neither a
191
+ # {DEFAULTS} key, nor `includes:`, nor a reserved namespace. Recorded rather than acted on: an
192
+ # unknown key stays as inert at run time as it has always been, and {ConfigAudit} turns the record
193
+ # into a warning. Empty for every conforming config.
194
+ #
195
+ # This exists because `#initialize` fetches each key it owns and never enumerates the rest, so by
196
+ # the time anything holds a Configuration the unknown keys are gone. The audit reads a
197
+ # Configuration, so without this the class of mistake it exists to catch — a value that silently
198
+ # resolves to nothing — was structurally invisible to it for whole keys.
199
+ attr_reader :unknown_keys
200
+
146
201
  attr_reader :target_ruby, :paths, :exclude_patterns, :plugins, :cache_path, :cache_max_bytes,
147
- :disabled_rules,
148
- :libraries, :signature_paths, :fold_platform_specific_paths,
202
+ :cache_validation, :disabled_rules,
203
+ :libraries, :signature_paths, :fold_platform_specific_paths, :parameter_inference,
149
204
  :plugins_io_network, :plugins_io_allowed_paths,
150
205
  :plugins_io_allowed_url_hosts,
151
206
  :severity_profile, :severity_overrides,
@@ -168,10 +223,67 @@ module Rigor
168
223
  # [PHPStan](https://phpstan.org/config-reference#paths).
169
224
  def self.load(path = nil)
170
225
  resolved = path || discover
171
- return new(DEFAULTS) if resolved.nil? || !File.exist?(resolved)
226
+ data =
227
+ if resolved.nil? || !File.exist?(resolved)
228
+ DEFAULTS
229
+ else
230
+ DEFAULTS.merge(load_with_includes(resolved))
231
+ end
232
+ new(autowire_default_plugins(data))
233
+ end
172
234
 
173
- data = load_with_includes(resolved)
174
- new(DEFAULTS.merge(data))
235
+ # ADR-93 WD2 — the one bundled plugin default-wired without a `plugins:` entry. `rigor-rbs-inline` is the
236
+ # gem; `rbs-inline` is its manifest id (the loader raises on a duplicate id, so both forms count as
237
+ # "already listed").
238
+ AUTOWIRED_RBS_INLINE_GEM = "rigor-rbs-inline"
239
+ AUTOWIRED_RBS_INLINE_ID = "rbs-inline"
240
+
241
+ # ADR-93 WD2 — default-wire the bundled `rigor-rbs-inline` plugin, in WD1's annotation-gated,
242
+ # magic-comment-free mode, when the upstream `rbs-inline` library is resolvable and the user has not
243
+ # already listed the plugin. Runs from {load} only — the real-project route — never from a bare
244
+ # {Configuration.new}, so the suite's unit constructions do not auto-wire. The gate is presence-based
245
+ # (ADR-72's shape): an annotation-free project pays only a comment scan and contributes nothing, so
246
+ # wiring it on cannot regress the run. This partially reverses the ADR-27/ADR-31 auto-load deferral for
247
+ # this one bundled plugin (the executed code is already vendored, not arbitrary third-party plugin code).
248
+ # Opt out with a `plugins:` entry disabling it (`enabled: false`), or per-file `# rbs_inline: disabled`.
249
+ def self.autowire_default_plugins(data)
250
+ entries = Array(data["plugins"])
251
+ return data if entries.any? { |entry| rbs_inline_plugin_entry?(entry) }
252
+ return data unless rbs_inline_library_resolvable?
253
+
254
+ merged = data.dup
255
+ merged["plugins"] = entries + [{
256
+ "gem" => AUTOWIRED_RBS_INLINE_GEM,
257
+ "id" => AUTOWIRED_RBS_INLINE_ID,
258
+ "config" => { "require_magic_comment" => false }
259
+ }]
260
+ merged
261
+ end
262
+
263
+ # True when a `plugins:` entry (String or Hash, string- or symbol-keyed) already references the
264
+ # `rigor-rbs-inline` plugin by gem name or by manifest id. Single-homed here so the CLI's
265
+ # `--treat-all-as-inline-rbs` injection and the WD2 auto-wire gate agree on what "already listed" means.
266
+ def self.rbs_inline_plugin_entry?(entry)
267
+ case entry
268
+ when String
269
+ entry == AUTOWIRED_RBS_INLINE_GEM
270
+ when Hash
271
+ string_keyed = entry.to_h { |k, v| [k.to_s, v] }
272
+ string_keyed["gem"] == AUTOWIRED_RBS_INLINE_GEM || string_keyed["id"] == AUTOWIRED_RBS_INLINE_ID
273
+ else
274
+ false
275
+ end
276
+ end
277
+
278
+ # A probe with no load side effect: it does not `require` the library, only asks whether RubyGems can
279
+ # resolve the name. `Gem::Specification.find_by_name` raises `Gem::MissingSpecError` (a `Gem::LoadError`)
280
+ # when the gem is absent — the standalone `gem install rigortype` case, WD3's residual. ADR-90's
281
+ # project-bundle fallback resolves the same name at the plugin's own `require` time.
282
+ def self.rbs_inline_library_resolvable?
283
+ Gem::Specification.find_by_name("rbs-inline")
284
+ true
285
+ rescue Gem::LoadError
286
+ false
175
287
  end
176
288
 
177
289
  # Returns the path to the config file Rigor would load under auto-discovery, or `nil` when neither
@@ -280,6 +392,10 @@ module Rigor
280
392
 
281
393
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
282
394
  def initialize(data = DEFAULTS)
395
+ # Record before the per-key fetches below discard the evidence. Top level only, deliberately —
396
+ # see {ConfigAudit.unknown_key_warnings} for why a nested check cannot key on DEFAULTS.
397
+ @unknown_keys = (data.keys.map(&:to_s) - KNOWN_KEYS).sort.freeze
398
+
283
399
  cache = DEFAULTS.fetch("cache").merge(data.fetch("cache", {}))
284
400
  plugins_io = DEFAULTS.fetch("plugins_io").merge(data.fetch("plugins_io", {}))
285
401
 
@@ -300,10 +416,14 @@ module Rigor
300
416
  @baseline_path = coerce_baseline_path(data.fetch("baseline", DEFAULTS.fetch("baseline")))
301
417
  @fold_platform_specific_paths = data.fetch(
302
418
  "fold_platform_specific_paths", DEFAULTS.fetch("fold_platform_specific_paths")
303
- ) == true
419
+ )
420
+ # ADR-67 WD6a — resolve to a strict Boolean so a truthy non-`true` value (e.g. a stray String) does not
421
+ # silently enable the gate; only the literal `true` activates the check-walk collector pre-pass.
422
+ @parameter_inference = data.fetch("parameter_inference", DEFAULTS.fetch("parameter_inference")) == true
304
423
  @cache_path = cache.fetch("path").to_s
305
424
  raw_max = cache.fetch("max_bytes")
306
425
  @cache_max_bytes = raw_max.nil? ? nil : Integer(raw_max)
426
+ @cache_validation = coerce_cache_validation(cache.fetch("validation", "auto"))
307
427
  @plugins_io_network = coerce_network_policy(plugins_io.fetch("network"))
308
428
  @plugins_io_allowed_paths = Array(plugins_io.fetch("allowed_paths")).map(&:to_s).freeze
309
429
  @plugins_io_allowed_url_hosts = Array(plugins_io.fetch("allowed_url_hosts")).map(&:to_s).freeze
@@ -340,6 +460,20 @@ module Rigor
340
460
  end
341
461
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
342
462
 
463
+ # Resolves the `cache.validation` tri-state to the boolean the run installs via
464
+ # {Cache::FileDigest.with_run}'s `strict:`. Explicit `"digest"` / `"stat"` win outright; the `"auto"`
465
+ # default is strict exactly when {CiDetector} recognises a CI provider (#190) — deliberately NOT stored at
466
+ # construction so the resolution honours the environment of the run, not of `Configuration.load`, and the
467
+ # frozen carrier stays env-independent. `RIGOR_STRICT_VALIDATION=1` is enforced separately inside
468
+ # {Cache::FileDigest.strict_validation?} and wins over all three values.
469
+ def cache_validation_strict?(env = ENV)
470
+ case cache_validation
471
+ when "digest" then true
472
+ when "stat" then false
473
+ else !CiDetector.detect(env).nil?
474
+ end
475
+ end
476
+
343
477
  def to_h # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
344
478
  {
345
479
  "target_ruby" => target_ruby,
@@ -351,9 +485,11 @@ module Rigor
351
485
  "signature_paths" => signature_paths,
352
486
  "pre_eval" => pre_eval,
353
487
  "fold_platform_specific_paths" => fold_platform_specific_paths,
488
+ "parameter_inference" => parameter_inference,
354
489
  "cache" => {
355
490
  "path" => cache_path,
356
- "max_bytes" => cache_max_bytes
491
+ "max_bytes" => cache_max_bytes,
492
+ "validation" => cache_validation
357
493
  },
358
494
  "plugins_io" => {
359
495
  "network" => plugins_io_network.to_s,
@@ -483,6 +619,18 @@ module Rigor
483
619
  value.to_s
484
620
  end
485
621
 
622
+ # ADR-87 WD1 — `cache.validation` is `"auto"` (default), `"stat"`, or `"digest"`. An unrecognised value
623
+ # fails soft to the default rather than aborting a check over a typo — the strict `"digest"` behaviour is
624
+ # the safe fallback either way, and the always-available `RIGOR_STRICT_VALIDATION=1` env escape hatch does
625
+ # not depend on this value being valid.
626
+ VALID_CACHE_VALIDATIONS = %w[auto stat digest].freeze
627
+ private_constant :VALID_CACHE_VALIDATIONS
628
+
629
+ def coerce_cache_validation(value)
630
+ str = value.to_s
631
+ VALID_CACHE_VALIDATIONS.include?(str) ? str : "auto"
632
+ end
633
+
486
634
  def coerce_network_policy(value)
487
635
  sym = value.to_sym
488
636
  unless VALID_NETWORK_POLICIES.include?(sym)
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Environment
5
+ # Slice A stdlib expansion. Stdlib libraries that `Environment.for_project` loads on top of RBS core
6
+ # unless the caller passes an explicit `libraries:` array. Each entry MUST be a stdlib library name
7
+ # accepted by `RBS::EnvironmentLoader#has_library?`; unknown libraries MUST fail-soft
8
+ # (`RbsLoader#build_env` already filters through `has_library?`). The default set covers the common
9
+ # stdlib surface a Ruby program is likely to import (`pathname`, `optparse`, `json`, `yaml`, `fileutils`,
10
+ # `tempfile`, `uri`, `logger`, `date`) plus the analyzer-adjacent gems shipping their own RBS in this
11
+ # bundle (`prism`, `rbs`). On hosts where one of these libraries is not installed, the loader silently
12
+ # drops it.
13
+ #
14
+ # Callers MAY add to the default by passing `libraries: %w[csv ...]`; the explicit list is appended to
15
+ # `DEFAULT_LIBRARIES` and de-duplicated. Callers that need a strictly RBS-core view MUST construct an
16
+ # `RbsLoader` directly instead of going through `for_project`.
17
+ #
18
+ # ADR-87 WD4 — extracted to its own light file (no engine requires) so the boot-slimming hit probe can
19
+ # reconstruct `Environment.for_project`'s merged library list — which feeds the run cache KEY's
20
+ # `rbs.libraries` slot — WITHOUT building the RBS environment (or loading the inference engine).
21
+ DEFAULT_LIBRARIES = %w[
22
+ pathname optparse json yaml fileutils tempfile tmpdir
23
+ stringio forwardable digest securerandom
24
+ uri logger date
25
+ pp delegate observable abbrev find tsort singleton
26
+ shellwords benchmark base64 did_you_mean
27
+ monitor mutex_m timeout
28
+ open3 erb etc ipaddr bigdecimal bigdecimal-math
29
+ prettyprint random-formatter time open-uri resolv
30
+ csv pstore objspace io-console cgi cgi-escape
31
+ strscan
32
+ prism rbs
33
+ ].freeze
34
+ end
35
+ end
@@ -90,10 +90,48 @@ module Rigor
90
90
  add_project_signatures(env, signature_paths)
91
91
  add_virtual_rbs(env, virtual_rbs)
92
92
  synthesize_missing_namespaces(env)
93
- resolved = env.resolve_type_names
93
+ env, resolved = resolve_quarantining_virtual_collisions(env, virtual_rbs)
94
94
  stub_missing_referenced_types(env, resolved, project_sig_files(signature_paths))
95
95
  end
96
96
 
97
+ # True when `content` parses as an RBS signature. {#virtual_rbs_collision_quarantined} uses this to
98
+ # tell a collision-dropped virtual entry (parses, but absent from the env) from a parse-failed one
99
+ # (the synthesizer's own WD6 skip, reported separately).
100
+ def parseable_rbs?(content)
101
+ ::RBS::Parser.parse_signature(::RBS::Buffer.new(name: "(rigor: virtual parse check)", content: content))
102
+ true
103
+ rescue ::RBS::BaseError
104
+ false
105
+ end
106
+
107
+ # Backstop for a virtual-vs-anything `RBS::DuplicatedDeclarationError` that only materialises at
108
+ # `resolve_type_names` (which rebuilds the env from `sources`). {.add_virtual_rbs}'s transactional
109
+ # rescue already handles the add-time case — empirically everything on rbs 4.x — but the rbs gemspec
110
+ # range spans `>= 3.0, < 5.0` (ADR-79) and WHERE duplicate detection fires is an rbs-internal choice
111
+ # this code must not depend on. Resolution rule is the same as the add-time path: the explicit
112
+ # signature wins, the colliding VIRTUAL buffer is dropped whole (`RBS::Environment#unload`) and
113
+ # resolution retries; every pass removes at least one virtual buffer, so the loop is bounded by the
114
+ # virtual-entry count. A duplicate involving no virtual buffer (sig-vs-sig), or an env without
115
+ # `#unload` (rbs 3.x), re-raises into the existing one-warning degrade path.
116
+ #
117
+ # The dropped set is not returned: consumers recover it from the built env via
118
+ # {#virtual_rbs_collision_quarantined}, which also works on a cache HIT where this build never ran.
119
+ def resolve_quarantining_virtual_collisions(env, virtual_rbs)
120
+ virtual_names = virtual_rbs.to_set { |name, _content| name.to_s }
121
+ (virtual_names.size + 1).times do
122
+ return [env, env.resolve_type_names]
123
+ rescue ::RBS::DuplicatedDeclarationError => e
124
+ raise unless env.respond_to?(:unload)
125
+
126
+ culprits = e.decls.filter_map { |decl| decl.location&.buffer&.name }
127
+ .uniq.select { |name| virtual_names.include?(name) }
128
+ raise if culprits.empty?
129
+
130
+ env = env.unload(culprits)
131
+ end
132
+ [env, env.resolve_type_names]
133
+ end
134
+
97
135
  # ADR-5 robustness, second tier. A project `signature_paths:` RBS that *references* a type no loaded
98
136
  # signature declares — `def x: () -> DRb::DRbServer` when the `drb` RBS is not available, or a stale
99
137
  # reference to its own removed `Textbringer::EditorError` — makes
@@ -356,9 +394,20 @@ module Rigor
356
394
  _, directives, decls = ::RBS::Parser.parse_signature(buffer)
357
395
  add_parsed_decls(env, buffer, directives, decls)
358
396
  rescue ::RBS::BaseError
359
- # WD6 fail-soft: a single broken virtual RBS contribution does not pull the whole env down. The
360
- # plugin layer records a `source-rbs-synthesis-failed` info diagnostic in slice 2; here we just
361
- # skip the entry.
397
+ # WD6 fail-soft: a single broken virtual RBS contribution does not pull the whole env down — for
398
+ # a parse error, skipping the entry is enough. But `RBS::Environment#add_source` appends to
399
+ # `env.sources` BEFORE inserting decls, so when the raise is a mid-insert
400
+ # `RBS::DuplicatedDeclarationError` (the entry declares a constant the project's own `sig/`
401
+ # already declares — the expected state for a project migrating between `sig/` and inline
402
+ # annotations, not an authoring error), the POISONED SOURCE is left behind, and
403
+ # `resolve_type_names` — which rebuilds the env from `sources` — re-raises the same error outside
404
+ # this rescue and collapses the WHOLE env to nil (measured on herb: 1,490 classes → 0, `require`
405
+ # itself stopped resolving, 74 false `call.unresolved-toplevel`). Make the skip transactional:
406
+ # drop the poisoned source. The explicit `.rbs` declaration wins — the spec keeps standalone
407
+ # `.rbs` files "the preferred place for complete type definitions" (`overview.md`) — and
408
+ # {#warn_about_virtual_rbs_collisions} names the dropped file. `sources` is the rbs 4.x shape;
409
+ # under the 3.x API this degrades to today's behaviour.
410
+ env.sources.reject! { |source| source.buffer.name == buffer.name } if env.respond_to?(:sources)
362
411
  end
363
412
  end
364
413
 
@@ -478,6 +527,55 @@ module Rigor
478
527
  names_synthesized_in(SYNTHETIC_NAMESPACE_BUFFER)
479
528
  end
480
529
 
530
+ # The project `signature_paths:` files that were QUARANTINED this run (they do not parse, so
531
+ # {RbsLoader.add_project_signatures} skipped them to keep the rest of the env alive), as
532
+ # `[absolute_path, first_error_line]` pairs. Memoised per loader: the detection re-parses only the user's
533
+ # own `sig/` set, but every consumer (the `rbs.coverage.quarantined-signature` diagnostic, `rigor doctor`,
534
+ # the stderr banner) reads it, and a cache HIT reaches it too — the env was built with the file already
535
+ # quarantined, so the condition is invisible in the cached env itself.
536
+ #
537
+ # @return [Array<Array(String, String)>] empty when every `signature_paths:` file parses.
538
+ def quarantined_signatures
539
+ @state[:quarantined] ||= self.class.quarantined_project_signatures(@signature_paths).freeze
540
+ end
541
+
542
+ # The total RBS-environment build failure captured this run, or nil when the env built. Unlike
543
+ # {#quarantined_signatures} — which the env survives, one file lighter, and which is re-derived by
544
+ # re-parsing so a cache HIT reports it too — a total failure (typically `RBS::DuplicatedDeclarationError`:
545
+ # a `signature_paths:` entry redeclaring a constant/class Rigor's bundled RBS already ships) collapses the
546
+ # WHOLE env to nil. A failed build produces no cached success to hide behind (nothing is persisted, so
547
+ # every run re-attempts and re-raises), so this is captured directly in {#env}'s rescue rather than
548
+ # re-derived. Forcing `env` (any query does) populates it.
549
+ #
550
+ # @return [Array(String, String, Array<String>), nil] `[error_class_name, first_error_line,
551
+ # conflicting_buffer_names]`, or nil when the environment built successfully.
552
+ def env_build_failure
553
+ env unless @state[:env_loaded]
554
+ @state[:env_build_failure]
555
+ end
556
+
557
+ # Virtual (inline-synthesized) contributions dropped by the collision quarantine
558
+ # ({.resolve_quarantining_virtual_collisions}): buffer names absent from the built env even though the
559
+ # entry's content is non-empty and parses (a parse failure is the synthesizer's own WD6 skip, reported
560
+ # through the synthesis reporter instead). Derived from the env rather than recorded during build —
561
+ # the {#quarantined_signatures} trick — so a cache HIT, which never runs the build, reports the same
562
+ # condition: the marshalled env simply lacks the dropped buffers.
563
+ #
564
+ # @return [Array<String>] virtual buffer names (source-file paths) whose contribution was dropped.
565
+ def virtual_rbs_collision_quarantined
566
+ @state[:virtual_rbs_collisions] ||= begin
567
+ built = @state[:env]
568
+ if built.nil? || @virtual_rbs.empty?
569
+ [].freeze
570
+ else
571
+ present = built.buffers.to_set(&:name)
572
+ @virtual_rbs.filter_map do |name, content|
573
+ name if !content.empty? && !present.include?(name) && self.class.parseable_rbs?(content)
574
+ end.freeze
575
+ end
576
+ end
577
+ end
578
+
481
579
  # The referenced-but-undeclared types {.stub_missing_referenced_types} stubbed so the project classes
482
580
  # that mention them could build (e.g. an unavailable `DRb::DRbServer`, or a stale
483
581
  # `Textbringer::EditorError`). Recovered off the built env like {#synthesized_namespaces}, so it
@@ -970,8 +1068,10 @@ module Rigor
970
1068
  @state[:env_loaded] = true
971
1069
  @state[:env] = cache_store ? cached_env : build_env
972
1070
  warn_about_quarantined_signatures
1071
+ warn_about_virtual_rbs_collisions
973
1072
  @state[:env]
974
1073
  rescue ::RBS::BaseError => e
1074
+ record_env_build_failure(e)
975
1075
  warn_about_env_build_failure_once(e)
976
1076
  @state[:env] = nil
977
1077
  end
@@ -982,10 +1082,15 @@ module Rigor
982
1082
  # is the visibility half of the fix: a shrinking diagnostic count must never be mistaken for a clean run
983
1083
  # when it actually means "your sig/ stopped loading". No-op when `signature_paths:` is empty (the cost is
984
1084
  # then a single empty-set check) or every file parses.
1085
+ #
1086
+ # `rigor check` ALSO reports this as the `rbs.coverage.quarantined-signature` diagnostic, which is what
1087
+ # reaches JSON / SARIF / CI annotations / the LSP. The banner is kept because the commands that build an
1088
+ # env WITHOUT producing a diagnostic stream (`coverage`, `sig-gen`) have no other channel, and a silently
1089
+ # degraded env is exactly what misleads there too.
985
1090
  def warn_about_quarantined_signatures
986
1091
  return if @state[:quarantine_warned]
987
1092
 
988
- quarantined = self.class.quarantined_project_signatures(@signature_paths)
1093
+ quarantined = quarantined_signatures
989
1094
  return if quarantined.empty?
990
1095
 
991
1096
  @state[:quarantine_warned] = true
@@ -1002,6 +1107,60 @@ module Rigor
1002
1107
  )
1003
1108
  end
1004
1109
 
1110
+ # The collision twin of {#warn_about_quarantined_signatures}: name, once per run, the source files
1111
+ # whose inline-synthesized RBS was dropped because it collides with a declaration another signature
1112
+ # source already made ({.resolve_quarantining_virtual_collisions} — the explicit `.rbs` wins). Without
1113
+ # this the drop is silent, and "my `#:` annotation does nothing" has no visible cause. Reads the
1114
+ # derived {#virtual_rbs_collision_quarantined}, so a cache HIT warns identically.
1115
+ def warn_about_virtual_rbs_collisions
1116
+ return if @state[:virtual_collision_warned]
1117
+
1118
+ dropped = virtual_rbs_collision_quarantined
1119
+ return if dropped.empty?
1120
+
1121
+ @state[:virtual_collision_warned] = true
1122
+ listed = dropped.first(QUARANTINE_WARN_LIMIT)
1123
+ more = dropped.size - listed.size
1124
+ lines = listed.map { |name| " - #{name}" }
1125
+ lines << " … and #{more} more" if more.positive?
1126
+ warn(
1127
+ "rigor: dropped inline-RBS contribution(s) from #{dropped.size} file(s): they declare a
1128
+ " \
1129
+ "constant, alias, or global that another signature source (typically the project's own
1130
+ " \
1131
+ "`sig/`) already declares. The explicit `.rbs` declaration wins, so inline annotations in
1132
+ " \
1133
+ "these files do not bind. Remove the duplication from either side to restore them:
1134
+ " \
1135
+ "#{lines.join("\n")}"
1136
+ )
1137
+ end
1138
+
1139
+ # Records the total RBS-environment build failure captured in {#env}'s rescue so the analysis layer can
1140
+ # surface it as the `rbs.coverage.environment-build-failed` diagnostic (the twin of
1141
+ # {#quarantined_signatures}: quarantine keeps the env alive minus one file, a total failure collapses the
1142
+ # WHOLE env to nil, so every type-of query degrades to `Dynamic[top]` and most rules stop firing). Stored
1143
+ # as `[error_class_name, first_error_line, conflicting_buffer_names]`. The buffer names are lifted off a
1144
+ # `RBS::DuplicatedDeclarationError#decls` — the typical failure, a `signature_paths:` entry redeclaring a
1145
+ # constant/class Rigor's bundled RBS already ships — so the diagnostic can name the colliding files.
1146
+ def record_env_build_failure(error)
1147
+ first_line = error.message.to_s.lines.first.to_s.strip
1148
+ @state[:env_build_failure] = [error.class.name, first_line, env_build_conflict_buffers(error)].freeze
1149
+ end
1150
+
1151
+ # The buffer (file) names carried by the colliding declarations of a `RBS::DuplicatedDeclarationError`, so
1152
+ # the diagnostic names the conflicting signature files rather than guessing. A buffer name is a `String`
1153
+ # path for a project `signature_paths:` file and a `Pathname` for a bundled RBS file, so each is coerced
1154
+ # to `String`. Other RBS build errors carry no `.decls`; they yield an empty list and the diagnostic
1155
+ # falls back to the message's first line alone.
1156
+ def env_build_conflict_buffers(error)
1157
+ return [].freeze unless error.respond_to?(:decls)
1158
+
1159
+ error.decls.filter_map { |decl| decl.location&.buffer&.name }.map(&:to_s).uniq.freeze
1160
+ rescue ::RBS::BaseError, StandardError
1161
+ [].freeze
1162
+ end
1163
+
1005
1164
  def warn_about_env_build_failure_once(error)
1006
1165
  return if @state[:env_build_warned]
1007
1166
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "digest"
4
4
 
5
+ require_relative "environment/default_libraries"
5
6
  require_relative "environment/class_registry"
6
7
  require_relative "environment/rbs_loader"
7
8
  require_relative "environment/reflection"
@@ -12,6 +13,7 @@ require_relative "environment/missing_gem_constant_index"
12
13
  require_relative "environment/bundle_sig_discovery"
13
14
  require_relative "environment/lockfile_resolver"
14
15
  require_relative "environment/rbs_collection_discovery"
16
+ require_relative "plugin/isolation"
15
17
  require_relative "environment/rbs_coverage_report"
16
18
  require_relative "inference/synthetic_method_index"
17
19
  require_relative "inference/project_patched_methods"
@@ -32,31 +34,8 @@ module Rigor
32
34
  DEFAULT_PROJECT_SIG_DIR = "sig"
33
35
  private_constant :DEFAULT_PROJECT_SIG_DIR
34
36
 
35
- # Slice A stdlib expansion. Stdlib libraries that `Environment.for_project` loads on top of RBS core
36
- # unless the caller passes an explicit `libraries:` array. Each entry MUST be a stdlib library name
37
- # accepted by `RBS::EnvironmentLoader#has_library?`; unknown libraries MUST fail-soft
38
- # (`RbsLoader#build_env` already filters through `has_library?`). The default set covers the common
39
- # stdlib surface a Ruby program is likely to import (`pathname`, `optparse`, `json`, `yaml`, `fileutils`,
40
- # `tempfile`, `uri`, `logger`, `date`) plus the analyzer-adjacent gems shipping their own RBS in this
41
- # bundle (`prism`, `rbs`). On hosts where one of these libraries is not installed, the loader silently
42
- # drops it.
43
- #
44
- # Callers MAY add to the default by passing `libraries: %w[csv ...]`; the explicit list is appended to
45
- # `DEFAULT_LIBRARIES` and de-duplicated. Callers that need a strictly RBS-core view MUST construct an
46
- # `RbsLoader` directly instead of going through `for_project`.
47
- DEFAULT_LIBRARIES = %w[
48
- pathname optparse json yaml fileutils tempfile tmpdir
49
- stringio forwardable digest securerandom
50
- uri logger date
51
- pp delegate observable abbrev find tsort singleton
52
- shellwords benchmark base64 did_you_mean
53
- monitor mutex_m timeout
54
- open3 erb etc ipaddr bigdecimal bigdecimal-math
55
- prettyprint random-formatter time open-uri resolv
56
- csv pstore objspace io-console cgi cgi-escape
57
- strscan
58
- prism rbs
59
- ].freeze
37
+ # `DEFAULT_LIBRARIES` is defined in the light `environment/default_libraries.rb` (required below) so the
38
+ # ADR-87 WD4 boot-slimming probe can read it without loading the inference engine.
60
39
 
61
40
  # ADR-72 — a Gemfile.lock gem name mapped to the opt-in plugin id that ships the SAME core-ext RBS. When
62
41
  # that plugin is loaded the auto-overlay for the gem stands down, so the two never both declare the
@@ -245,9 +224,9 @@ module Rigor
245
224
  # ADR-82 WD9 — the resolved bundle root, so the missing-gem constant index reads each RBS-less gem's
246
225
  # entry file from the TARGET's bundle (not rigor's own — see `MissingGemConstantIndex`). Resolved
247
226
  # once here; passed through to the lazy index build.
248
- bundle_root = BundleSigDiscovery.resolve_bundle_path(
227
+ bundle_root = resolve_target_bundle_root(
249
228
  bundle_path: bundler_bundle_path, project_root: root, auto_detect: bundler_auto_detect
250
- )&.to_s
229
+ )
251
230
  # O4 Layer 3 slice 2 — when `rbs collection install` has been run for the target project, parse the
252
231
  # resulting `rbs_collection.lock.yaml` and feed each gem's `<collection_path>/<name>/<version>/`
253
232
  # directory into `signature_paths:`. Stdlib-typed entries are skipped (already covered by
@@ -327,6 +306,19 @@ module Rigor
327
306
  sig.directory? ? [sig] : []
328
307
  end
329
308
 
309
+ # ADR-82 WD9 — the resolved bundle root for the missing-gem constant index, doubling (ADR-90) as the
310
+ # hand-off to the plugin isolation layer: ADR-39 target-library invocation (`Plugin::Inflector` →
311
+ # activesupport, the Rack status table) can then require the analyzed project's own locked gems when
312
+ # Rigor's host gem environment does not carry them — the standalone `gem install rigortype` case,
313
+ # where activesupport is deliberately not a runtime dependency.
314
+ def resolve_target_bundle_root(bundle_path:, project_root:, auto_detect:)
315
+ bundle_root = BundleSigDiscovery.resolve_bundle_path(
316
+ bundle_path: bundle_path, project_root: project_root, auto_detect: auto_detect
317
+ )&.to_s
318
+ Plugin::Isolation.target_bundle_root = bundle_root
319
+ bundle_root
320
+ end
321
+
330
322
  # The `:missing` classification (locked gems with no RBS through any resolution path), computed once
331
323
  # and consumed twice: as `[gem_name, version]` pairs for the ADR-82 WD9 constant-ownership index, and
332
324
  # as the eligible-gem set for the ADR-72 overlay resolution. Returns `[pairs, overlay_paths]`.