rigortype 0.3.8 → 0.4.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 (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -4,7 +4,9 @@ require "rbs"
4
4
 
5
5
  require_relative "../type"
6
6
  require_relative "../inference/rbs_type_translator"
7
+ require_relative "../builtins/imported_refinements"
7
8
  require_relative "rbs_hierarchy"
9
+ require_relative "member_consistency"
8
10
 
9
11
  module Rigor
10
12
  class Environment
@@ -42,6 +44,19 @@ module Rigor
42
44
  # cache HIT. Never a real path, so it must never be reported as one (issue #696).
43
45
  CACHED_LOCATION_BUFFER_NAME = "<cached>"
44
46
 
47
+ # Issue #527 — the two directory trees whose `.rbs` files ARE Ruby's own surface: the `rbs` gem's
48
+ # `core/` and its bundled `stdlib/` repository. {#core_stdlib_classes} tests a declaration's file
49
+ # against them. Resolved once at load time, on the main Ractor: reading these constants walks a
50
+ # chain of non-`Ractor.shareable?` module constants and raises `Ractor::IsolationError` inside a
51
+ # worker (see {#prewarm}). Empty when the rbs version in use does not expose them, which switches
52
+ # the feature off rather than widening it.
53
+ CORE_STDLIB_ROOTS = begin
54
+ [::RBS::EnvironmentLoader::DEFAULT_CORE_ROOT, ::RBS::Repository::DEFAULT_STDLIB_ROOT]
55
+ .compact.map { |root| "#{root}/".freeze }.freeze
56
+ rescue StandardError
57
+ [].freeze
58
+ end
59
+
45
60
  # Cap on how many quarantined `signature_paths:` files {#warn_about_quarantined_signatures} lists by name
46
61
  # before collapsing the tail to "… and N more" — a broken generator can emit many, and a wall of parse
47
62
  # errors buries the signal.
@@ -83,10 +98,23 @@ module Rigor
83
98
  # mastodon coverage note). Per-file loading quarantines the broken file instead. Vendored / core-overlay
84
99
  # sigs are Rigor-shipped and trusted, so they stay on the loader's fast batch path.
85
100
  add_bundled_signatures(rbs_loader, loaded_libraries.to_set(&:to_s))
86
- env = RBS::Environment.from_loader(rbs_loader)
101
+ env = unload_upstream_core_shims(RBS::Environment.from_loader(rbs_loader), rbs_loader)
87
102
  project_files = project_sig_files(signature_paths)
88
- add_project_signatures(env, signature_paths, deferred_signature_paths)
89
- add_virtual_rbs(env, virtual_rbs)
103
+ # Issue #1075 — decided from the inputs before either side enters `env`: the consistency rule may
104
+ # stand the `.rbs` member down, and that has to happen before its file is added.
105
+ consistency = member_consistency_for(project_files, virtual_rbs)
106
+ add_project_signatures(env, signature_paths, deferred_signature_paths,
107
+ standdowns: consistency.signature_standdowns)
108
+ # Issue #928 — the capability-role catalog goes in AFTER the project's own signatures, per
109
+ # declaration, so a project that already declares one of these interface names keeps its own
110
+ # (`add_capability_role_signatures`). The rest of the bundled RBS cannot be ordered that way (it
111
+ # is what `from_loader` builds the env FROM), which is why a colliding project file is quarantined
112
+ # instead — and losing a whole `sig/` file to a name as ordinary as `_Closable` is exactly what
113
+ # shipping a top-level interface catalog would otherwise cost.
114
+ add_capability_role_signatures(env)
115
+ # Issues #824 / #1075 — every member a `.rbs` file also declares was ranked above; the inline side of
116
+ # each pair the `.rbs` won stands down here ({.add_virtual_rbs}), so the two never collide.
117
+ add_virtual_rbs(env, virtual_rbs, inline_standdowns: consistency.inline_standdowns)
90
118
  synthesize_missing_namespaces(env)
91
119
  # Issue #777 — resolve-time backstop: also unload colliding PROJECT buffers (class-vs-module /
92
120
  # constant redeclarations against bundled RBS). Virtual culprits are preferred when both appear.
@@ -119,8 +147,8 @@ module Rigor
119
147
  BIG_MATH_SIG_BASENAME = "big_math.rbs"
120
148
  private_constant :BIG_MATH_SIG_BASENAME
121
149
 
122
- # @param libraries [Array<String>] the resolved library list, `DEFAULT_LIBRARIES` included.
123
- # @return [Array<String>] the same list, minus `bigdecimal-math` when `bigdecimal` already brings
150
+ # @param libraries — the resolved library list, `DEFAULT_LIBRARIES` included.
151
+ # @return the same list, minus `bigdecimal-math` when `bigdecimal` already brings
124
152
  # `BigMath` in.
125
153
  def libraries_without_shadowed_bigdecimal_math(libraries)
126
154
  return libraries unless libraries.include?(BIGDECIMAL_LIBRARY) && libraries.include?(BIGDECIMAL_MATH_LIBRARY)
@@ -145,6 +173,55 @@ module Rigor
145
173
  false
146
174
  end
147
175
 
176
+ # Issue #1109 — the `rbs` gem (a `DEFAULT_LIBRARIES` entry, so in every run) ships the shims its
177
+ # own Steep check needs under `sig/shims/`, and one of them reopens a core module: `enumerable.rbs`
178
+ # prepends a `(2) -> Enumerator[[Elem, Elem], void]` overload to `Enumerable#each_slice`. That is
179
+ # a claim about rbs's own call sites, not about Ruby — `[1, 2, 3].each_slice(2)` yields `[3]` last
180
+ # — and it leaked into every analysed project: the blockless form typed as a fixed pair (so a
181
+ # `nil` guard on the second slot folded away) and the block form returned `void` instead of the
182
+ # receiver. Unloading the file leaves core's `Enumerator[Array[Elem], self]`, which agrees with the
183
+ # block form's `Array[Elem]`. The other shims (`bundler.rbs`, `rubygems.rbs`) only add missing
184
+ # declarations and stay. Fails soft to the env as built: a relocated or absent shim, or an rbs API
185
+ # shape this code does not recognise, keeps the pre-#1109 behaviour rather than failing the build.
186
+ # Because errors are rescued, the guard spec in `rbs_loader_spec.rb` is what catches a broken unload,
187
+ # so it must stay green on both lines of the RBS compatibility matrix.
188
+ RBS_LIBRARY = "rbs"
189
+ private_constant :RBS_LIBRARY
190
+ UNSOUND_CORE_SHIMS = %w[shims/enumerable.rbs].freeze
191
+ private_constant :UNSOUND_CORE_SHIMS
192
+
193
+ def unload_upstream_core_shims(env, rbs_loader)
194
+ return env unless env.respond_to?(:unload)
195
+
196
+ shim_names = upstream_core_shim_names(rbs_loader)
197
+ return env if shim_names.empty?
198
+
199
+ buffers = loaded_signature_buffers(env)&.select { |buffer| shim_names.include?(buffer.name.to_s) }
200
+ buffers.nil? || buffers.empty? ? env : env.unload(buffers)
201
+ rescue StandardError
202
+ env
203
+ end
204
+
205
+ # rbs 4.x lists its sources through `each_rbs_source`; rbs 3.x keys `signatures` by `Buffer`, and its
206
+ # `unload` matches those very objects with `include?`, so the env's own keys are what it must receive.
207
+ def loaded_signature_buffers(env)
208
+ if env.respond_to?(:each_rbs_source)
209
+ env.each_rbs_source.map(&:buffer)
210
+ elsif env.respond_to?(:signatures)
211
+ env.signatures.keys
212
+ end
213
+ end
214
+
215
+ def upstream_core_shim_names(rbs_loader)
216
+ names = Set.new
217
+ rbs_loader.each_dir do |source, dir|
218
+ next unless source.respond_to?(:name) && source.name == RBS_LIBRARY
219
+
220
+ UNSOUND_CORE_SHIMS.each { |relative| names << Pathname(dir).join(relative).to_s }
221
+ end
222
+ names
223
+ end
224
+
148
225
  # True when `content` parses as an RBS signature. {#virtual_rbs_collision_quarantined} uses this to
149
226
  # tell a collision-dropped virtual entry (parses, but absent from the env) from a parse-failed one
150
227
  # (the synthesizer's own WD6 skip, reported separately).
@@ -323,16 +400,21 @@ module Rigor
323
400
  # Buffer names are the file's absolute path (matching {.project_sig_files}) so {.project_entry?} — which
324
401
  # attributes a `class_decls` entry to the project by buffer name — still recognises these declarations.
325
402
  # Sorted for a deterministic add order (the env feeds the cache, ADR-54).
326
- def add_project_signatures(env, signature_paths, deferred_paths = [])
403
+ #
404
+ # @param standdowns — issue #1075 — `{signature_path => Set[[class, method, kind]]}`, the members an
405
+ # inline declaration displaced because it was the more precise side of a consistent pair
406
+ # ({MemberConsistency::Resolution#signature_standdowns}). They are removed before the file is added.
407
+ def add_project_signatures(env, signature_paths, deferred_paths = [], standdowns: {})
327
408
  deferred = project_sig_files(deferred_paths)
328
409
  (project_sig_files(signature_paths) - deferred).sort.each do |file|
329
410
  parsed = parse_signature_file(file)
330
411
  next if parsed.nil? # quarantined (unparseable) or unreadable — skip so the env survives
331
412
 
332
413
  buffer, directives, decls = parsed
414
+ strip_members!(decls, standdowns[file]) if standdowns.key?(file)
333
415
  add_project_parsed_decls(env, buffer, directives, decls)
334
416
  end
335
- add_deferred_signatures(env, deferred)
417
+ add_deferred_signatures(env, deferred, standdowns)
336
418
  end
337
419
 
338
420
  # Issue #777 — add one project signature transactionally. `RBS::Environment#add_source` appends to
@@ -370,7 +452,7 @@ module Rigor
370
452
  # (rigor-activerecord's is `relation.rbs`, declaring exactly `Relation`), so dropping the file drops
371
453
  # exactly the conflicting declaration. Splitting a plugin file that mixes a colliding class with
372
454
  # innocent ones is the change to make if that ever stops being true.
373
- def add_deferred_signatures(env, deferred_files)
455
+ def add_deferred_signatures(env, deferred_files, standdowns = {})
374
456
  deferred_files.sort.each do |file|
375
457
  parsed = parse_signature_file(file)
376
458
  next if parsed.nil?
@@ -378,12 +460,13 @@ module Rigor
378
460
  buffer, directives, decls = parsed
379
461
  next if generic_arity_conflict(env, decls)
380
462
 
463
+ strip_members!(decls, standdowns[file]) if standdowns.key?(file)
381
464
  add_project_parsed_decls(env, buffer, directives, decls)
382
465
  end
383
466
  end
384
467
 
385
468
  # Issue #610 — which deferred (plugin-contributed) files stood down, as
386
- # `[absolute_path, class_name, existing_arity, incoming_arity]`, sorted. DERIVED from a built env
469
+ # `[absolute_path, class_name, existing_arity, incoming_arity, existing_file]`, sorted. DERIVED from a built env
387
470
  # rather than recorded during the build, for {RbsLoader#quarantined_signatures}' reason: a cache HIT
388
471
  # never runs the build, and a condition only the build knew would silently disappear on the second
389
472
  # run. Re-asking the same question of the FINAL env gives the same answer — the file's declaration is
@@ -400,21 +483,40 @@ module Rigor
400
483
  end
401
484
  end
402
485
 
403
- # The first `[class_name, existing_arity, incoming_arity]` where `decls` would re-declare a class the
404
- # env already has with a DIFFERENT number of type parameters, or nil when they agree everywhere.
405
- # Equal arity is not a conflict: RBS reopens the class, which is the ordinary and supported case, so
406
- # the stand-down must not fire on it.
486
+ # The first `[class_name, existing_arity, incoming_arity, existing_file]` where `decls` would
487
+ # re-declare a class the env already has with a DIFFERENT number of type parameters, or nil when
488
+ # they agree everywhere. Equal arity is not a conflict: RBS reopens the class, which is the ordinary
489
+ # and supported case, so the stand-down must not fire on it.
490
+ #
491
+ # The existing arity is read off the entry's FIRST declaration, never through `entry.type_params`:
492
+ # rbs 4.x's `ClassEntry#type_params` validates every declaration against the first before answering
493
+ # and raises `GenericParameterMismatchError` on an entry that already disagrees with itself (rbs
494
+ # 3.x's does not), so the derived reader ({.deferred_standdowns}) raised on exactly the env it exists
495
+ # to describe — one a user's own two sources collided, or one the cache served from before the
496
+ # producer passed the deferred list. The first declaration is also the one rbs compares against.
497
+ # `existing_file` is that declaration's buffer name — the source that displaced the plugin's — or
498
+ # nil when it has none or carries only the cached-position sentinel.
407
499
  def generic_arity_conflict(env, decls)
408
500
  each_declared_class(decls) do |name, arity|
409
501
  entry = env.class_decls[::RBS::TypeName.parse(name)]
410
- next if entry.nil? || !entry.respond_to?(:type_params)
502
+ next if entry.nil?
411
503
 
412
- existing = entry.type_params&.size
413
- return [name, existing, arity] if existing && existing != arity
504
+ first = entry_declarations(entry).first
505
+ existing = first.respond_to?(:type_params) ? first.type_params&.size : nil
506
+ return [name, existing, arity, declaration_buffer_name(first)] if existing && existing != arity
414
507
  end
415
508
  nil
416
509
  end
417
510
 
511
+ def declaration_buffer_name(decl)
512
+ location = decl.respond_to?(:location) ? decl.location : nil
513
+ buffer = location&.buffer
514
+ name = buffer.nil? ? nil : buffer.name.to_s
515
+ return nil if name.nil? || name.empty? || name == CACHED_LOCATION_BUFFER_NAME
516
+
517
+ name
518
+ end
519
+
418
520
  # Yields `[absolute_class_name, type_param_count]` for every class declared anywhere in `decls`,
419
521
  # descending through modules and nested classes. Names inside a `module`/`class` body are parsed
420
522
  # RELATIVE, so the enclosing path is accumulated here — the env is keyed by absolute name, and
@@ -798,7 +900,13 @@ module Rigor
798
900
  # provenance (RBS parse errors cite it) — it is not a real file path. Per WD6 the synthesizer-emit
799
901
  # path is responsible for catching its own parse errors and returning `nil` rather than garbage; this
800
902
  # method assumes its input is parseable and only rescues `RBS::ParsingError` as a fail-soft.
801
- def add_virtual_rbs(env, virtual_rbs)
903
+ # @param inline_standdowns — issue #1075 — the `[class, method, kind]` keys whose inline member
904
+ # stands down because a project `.rbs` declares the same member and ADR-112 WD5's consistency rule
905
+ # let the `.rbs` bind ({MemberConsistency::Resolution#inline_standdowns}). Removing them before the
906
+ # declaration is added keeps the two from colliding and costing the whole class its method surface.
907
+ # The outcome is reported, never silent: {RbsLoader#member_consistency} re-derives the same records
908
+ # for the run's rows.
909
+ def add_virtual_rbs(env, virtual_rbs, inline_standdowns: Set.new)
802
910
  return if virtual_rbs.nil? || virtual_rbs.empty?
803
911
 
804
912
  virtual_rbs.each do |filename, content|
@@ -809,6 +917,7 @@ module Rigor
809
917
 
810
918
  buffer = ::RBS::Buffer.new(name: filename.to_s, content: content.to_s)
811
919
  _, directives, decls = ::RBS::Parser.parse_signature(buffer)
920
+ strip_members!(decls, inline_standdowns) unless inline_standdowns.empty?
812
921
  add_parsed_decls(env, buffer, directives, decls)
813
922
  rescue ::RBS::BaseError
814
923
  # WD6 fail-soft: a single broken virtual RBS contribution does not pull the whole env down — for
@@ -828,6 +937,204 @@ module Rigor
828
937
  end
829
938
  end
830
939
 
940
+ # Issue #1075 / ADR-112 WD5 — the member-level half of inline-vs-`sig/` consistency, which replaced
941
+ # #824's "`sig/` wins per member" (ADR-32 WD13).
942
+ #
943
+ # A method declared BOTH by a project `.rbs` and by an inline annotation in its own `.rb` lands twice
944
+ # in one `RBS::Environment`: rbs merges the two sources into a single `ClassEntry` and ranks neither,
945
+ # so `RBS::DefinitionBuilder::MethodBuilder::Methods#validate!` raises
946
+ # `RBS::DuplicatedMethodDefinitionError` and the WHOLE class loses its method surface — every call on
947
+ # it, real methods and typos alike, reads `Dynamic[top]`. Measured on Rigor's own tree while
948
+ # [#779](https://github.com/rigortype/rigor/pull/779) was evaluated: 17 of 234 annotated files
949
+ # overlapped `sig/`, and the collision took 44 classes down. Steep 2.1 / rbs 4.1 have no precedence
950
+ # rule to follow here (ADR-32 WD13), so exactly one of the two members has to go before the
951
+ # environment is built. {MemberConsistency} decides which: the more precise of two consistent
952
+ # declarations binds, and on a contradiction or an undecidable pair the `.rbs` does.
953
+ #
954
+ # @return a {MemberConsistency::Resolution}. {MemberConsistency::EMPTY} — and free — for a project
955
+ # that contributes no inline RBS, which is every project without a `source_rbs_synthesizer:` plugin.
956
+ #
957
+ # Derived from the loader's INPUTS — the synthesized sources and the project's signature files —
958
+ # never from a built environment, for {RbsLoader#quarantined_signatures}' reason turned inside out: a
959
+ # cache HIT never runs the build, and a stripped member leaves no trace in the environment to read
960
+ # back. The build and {RbsLoader#member_consistency} call this same function on the same two inputs,
961
+ # so the members they strip and the rows they report agree warm and cold.
962
+ def member_consistency_for(project_files, virtual_rbs, proof: nil)
963
+ return MemberConsistency::EMPTY if virtual_rbs.nil? || virtual_rbs.empty?
964
+
965
+ signature_members, shadowable = signature_member_index(project_files, virtual_rbs)
966
+ inline_members = inline_member_entries(virtual_rbs, all: !signature_members.empty?)
967
+ MemberConsistency.resolve(signature_members, inline_members, shadowable: shadowable, proof: proof)
968
+ end
969
+
970
+ # `[owners, shadowable]`. `owners` is `{[class_name, method_name, kind] => [signature_path, member,
971
+ # visibility]}` for every method-shaped member the project's own `signature_paths:` files declare and
972
+ # the inline sources also name; the first file in sorted order owns a key two `.rbs` files declare
973
+ # (that pair fails the definition build anyway). `shadowable` is every `::` segment of every class /
974
+ # module name the inline sources and the signature files declare — the names a relative reference
975
+ # might resolve to inside an enclosing namespace ({MemberConsistency::Comparator#faithful?}).
976
+ #
977
+ # The parse is scoped by a substring pre-filter over each file's text: a declaration has to spell its
978
+ # own name, so a signature file mentioning none of the names the inline sources declare cannot
979
+ # collide with them. The filter over-approximates (it matches a name in a comment or a type position
980
+ # too), so it can only cost an unnecessary parse, never miss a collision.
981
+ def signature_member_index(project_files, virtual_rbs)
982
+ names = inline_declared_name_segments(virtual_rbs)
983
+ return [{}, names] if project_files.nil? || project_files.empty? || names.empty?
984
+
985
+ shadowable = names.dup
986
+ owners = project_files.to_a.sort.each_with_object({}) do |file, index|
987
+ content = read_signature_text(file)
988
+ next if content.nil?
989
+
990
+ merge_declared_segments(content, shadowable)
991
+ next if names.none? { |name| content.include?(name) }
992
+
993
+ index_signature_members(index, file, parse_signature_source(file, content))
994
+ end
995
+ [owners, shadowable]
996
+ end
997
+
998
+ def index_signature_members(index, file, decls)
999
+ return if decls.nil?
1000
+
1001
+ each_declared_member(decls) do |class_name, member, visibility|
1002
+ member_method_keys(member).each do |method_name, kind|
1003
+ index[[class_name, method_name, kind]] ||= [file, member, visibility].freeze
1004
+ end
1005
+ end
1006
+ end
1007
+
1008
+ # The directive heads a refinement {MemberConsistency.refinement_conflicts} checks, used to skip
1009
+ # parsing an inline source that carries none when no `.rbs` overlaps it. Narrower than `rigor:v1:` on
1010
+ # purpose: rbs-inline's output marks every skeleton `%a{rigor:v1:inferred-return}`, so that prefix is
1011
+ # in nearly every synthesized source.
1012
+ REFINEMENT_DIRECTIVE_MARKS = ["rigor:v1:return:", "rigor:v1:param:"].freeze
1013
+ private_constant :REFINEMENT_DIRECTIVE_MARKS
1014
+
1015
+ # `[[virtual_name, class_name, member], ...]` for the inline members {MemberConsistency.resolve}
1016
+ # reads: every member when a `.rbs` file may overlap them (`all:`), otherwise only the members of a
1017
+ # source that carries a `rigor:v1:return:` / `rigor:v1:param:` refinement, which is checked against
1018
+ # its own declared type. Entries {.add_virtual_rbs} would skip are skipped here too.
1019
+ def inline_member_entries(virtual_rbs, all:)
1020
+ virtual_rbs.each_with_object([]) do |(name, content), entries|
1021
+ content = content.to_s
1022
+ next if content.empty? || invalid_encoding?(content)
1023
+ next unless all || REFINEMENT_DIRECTIVE_MARKS.any? { |mark| content.include?(mark) }
1024
+
1025
+ decls = parse_signature_source(name, content)
1026
+ next if decls.nil?
1027
+
1028
+ each_declared_member(decls) do |class_name, member, visibility|
1029
+ entries << [name.to_s, class_name, member, visibility]
1030
+ end
1031
+ end
1032
+ end
1033
+
1034
+ # Removes, in place, every member contributing a key in `keys` (a Set of `[class, method, kind]`).
1035
+ # Nested declarations are descended into after the rejection, and a declaration member contributes no
1036
+ # method key, so nesting is never itself removed.
1037
+ def strip_members!(decls, keys, prefix = [])
1038
+ Array(decls).each do |decl|
1039
+ next unless declaration_with_members?(decl)
1040
+
1041
+ inner = prefix + [decl.name.to_s.delete_prefix("::")]
1042
+ class_name = inner.join("::")
1043
+ decl.members.reject! do |member|
1044
+ member_method_keys(member).any? { |method_name, kind| keys.include?([class_name, method_name, kind]) }
1045
+ end
1046
+ strip_members!(decl.members, keys, inner)
1047
+ end
1048
+ end
1049
+
1050
+ # Yields `[class_name, member, visibility]` for every member of every class / module declaration in
1051
+ # `decls`, descending through nesting. The enclosing path is accumulated and the name rendered
1052
+ # `::`-stripped, so a `module Foo; class Bar` and a `class Foo::Bar` elsewhere key the same — which is
1053
+ # what makes a `sig/` declaration and an inline one comparable at all. `visibility` is the member's
1054
+ # own (`private def x`) or else the one the enclosing `private` / `public` section sets; a singleton
1055
+ # method takes no section, as in Ruby.
1056
+ def each_declared_member(decls, prefix = [], &block)
1057
+ Array(decls).each do |decl|
1058
+ next unless declaration_with_members?(decl)
1059
+
1060
+ inner = prefix + [decl.name.to_s.delete_prefix("::")]
1061
+ class_name = inner.join("::")
1062
+ section = :public
1063
+ decl.members.each do |member|
1064
+ case member
1065
+ when ::RBS::AST::Members::Private then section = :private
1066
+ when ::RBS::AST::Members::Public then section = :public
1067
+ else block.call(class_name, member, member_visibility(member, section))
1068
+ end
1069
+ end
1070
+ each_declared_member(decl.members, inner, &block)
1071
+ end
1072
+ end
1073
+
1074
+ def member_visibility(member, section)
1075
+ own = member.respond_to?(:visibility) ? member.visibility : nil
1076
+ return own if own
1077
+
1078
+ member.respond_to?(:kind) && member.kind == :singleton ? :public : section
1079
+ end
1080
+
1081
+ def declaration_with_members?(decl)
1082
+ (decl.is_a?(::RBS::AST::Declarations::Module) || decl.is_a?(::RBS::AST::Declarations::Class)) &&
1083
+ decl.respond_to?(:members)
1084
+ end
1085
+
1086
+ # {MemberConsistency.member_method_keys} — the `[method_name, kind]` pairs a member contributes to a
1087
+ # definition build, which is what decides whether two members collide.
1088
+ def member_method_keys(member)
1089
+ MemberConsistency.member_method_keys(member)
1090
+ end
1091
+
1092
+ # A class / module declaration header, as the pre-filter in {.signature_member_index} reads it out of
1093
+ # synthesized RBS text.
1094
+ INLINE_DECLARATION_NAME = /^[ \t]*(?:class|module)[ \t]+([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)/
1095
+ private_constant :INLINE_DECLARATION_NAME
1096
+
1097
+ # Every `::`-separated segment of every class / module name the inline sources declare, read off the
1098
+ # synthesized TEXT rather than a parse: the set only has to be a SUPERSET (it feeds the pre-filter),
1099
+ # and a regexp over the handful of annotated files is far cheaper than parsing them a second time.
1100
+ # Entries {.add_virtual_rbs} would skip are skipped here too — an empty or invalid-UTF-8 contribution
1101
+ # declares nothing, and `String#scan` raises `ArgumentError` on invalid bytes rather than returning
1102
+ # no match.
1103
+ def inline_declared_name_segments(virtual_rbs)
1104
+ virtual_rbs.each_with_object(Set.new) do |(_name, content), acc|
1105
+ content = content.to_s
1106
+ next if content.empty? || invalid_encoding?(content)
1107
+
1108
+ merge_declared_segments(content, acc)
1109
+ end
1110
+ end
1111
+
1112
+ # Adds every `::` segment of every class / module header in `content` to `segments`. The pattern has
1113
+ # one capture group, which every match fills.
1114
+ def merge_declared_segments(content, segments)
1115
+ content.scan(INLINE_DECLARATION_NAME) { segments.merge(Regexp.last_match(1).to_s.split("::")) }
1116
+ end
1117
+
1118
+ # One project signature file's text, or nil when it is unreadable or not valid UTF-8.
1119
+ def read_signature_text(file)
1120
+ content = File.read(file, encoding: "UTF-8")
1121
+ invalid_encoding?(content) ? nil : content
1122
+ rescue Errno::ENOENT, Errno::EISDIR, Errno::EACCES
1123
+ nil
1124
+ end
1125
+
1126
+ # The declarations of already-read RBS text, or nil when it does not parse. The twin of
1127
+ # {.parse_signature_file} for content the caller already holds (a synthesized virtual source, or a
1128
+ # signature file the pre-filter has just read).
1129
+ def parse_signature_source(name, content)
1130
+ _buffer, _directives, decls = ::RBS::Parser.parse_signature(
1131
+ ::RBS::Buffer.new(name: name.to_s, content: content)
1132
+ )
1133
+ decls
1134
+ rescue ::RBS::BaseError
1135
+ nil
1136
+ end
1137
+
831
1138
  # Per-gem `data/vendored_gem_sigs/<gem>/` directories that ship with Rigor. Each subdirectory is one
832
1139
  # gem's RBS surface (the `<gem>.rbs` file is the typical content; `LICENSE.upstream` records
833
1140
  # provenance). Coverage is deliberately scoped to the native-extension and "everywhere in Rails" gems
@@ -840,9 +1147,10 @@ module Rigor
840
1147
  private_constant :VENDORED_GEM_SIGS_ROOT
841
1148
 
842
1149
  # Rigor-owned core-overlay RBS (`data/core_overlay/`). Reopens Ruby core classes to add methods
843
- # upstream `ruby/rbs` omits but which every concrete value answers at runtime — loaded last so
844
- # upstream always wins on conflict. Public so the cache descriptor can digest these files into the
845
- # env-blob key.
1150
+ # upstream `ruby/rbs` omits but which every concrete value answers at runtime. A direct `def` that
1151
+ # another source also declares is a `DuplicatedMethodDefinitionError`, not a precedence question, so a
1152
+ # method a project `sig/` or a later rbs is likely to declare goes on a module the class extends
1153
+ # (`process.rbs`). Public so the cache descriptor can digest these files into the env-blob key.
846
1154
  CORE_OVERLAY_SIGS_ROOT = File.expand_path(
847
1155
  "../../../data/core_overlay",
848
1156
  __dir__
@@ -854,6 +1162,51 @@ module Rigor
854
1162
  [Pathname(CORE_OVERLAY_SIGS_ROOT)]
855
1163
  end
856
1164
 
1165
+ # Rigor's capability-role catalog (`data/capability_roles/`) — the structural interfaces ADR-1 and
1166
+ # `docs/type-specification/structural-interfaces-and-object-shapes.md` name as shipped, so a
1167
+ # `%a{rigor:v1:conforms-to _ClosableStream}` written by following the specification resolves in a
1168
+ # default run instead of failing soft to an unresolved-interface row. Public for the same reason
1169
+ # {.core_overlay_sig_paths} is: the cache descriptor digests these files into the env-blob key.
1170
+ CAPABILITY_ROLES_SIGS_ROOT = File.expand_path(
1171
+ "../../../data/capability_roles",
1172
+ __dir__
1173
+ ).freeze
1174
+
1175
+ def capability_role_sig_paths
1176
+ return [] unless File.directory?(CAPABILITY_ROLES_SIGS_ROOT)
1177
+
1178
+ [Pathname(CAPABILITY_ROLES_SIGS_ROOT)]
1179
+ end
1180
+
1181
+ # Adds the catalog to an env that already holds the project's own signatures, one INTERFACE at a
1182
+ # time, skipping any name the env already declares. Granularity is the declaration rather than the
1183
+ # file (the #610 stand-down's unit) because this one file carries five unrelated roles: a project
1184
+ # declaring `_Closable` must not also lose `_ClosableStream`.
1185
+ def add_capability_role_signatures(env)
1186
+ declared = declared_interface_names(env)
1187
+ capability_role_sig_paths.each do |dir|
1188
+ dir.children.sort.each do |file|
1189
+ next unless file.file? && file.extname == ".rbs"
1190
+
1191
+ parsed = parse_signature_file(file.to_s)
1192
+ next if parsed.nil?
1193
+
1194
+ buffer, directives, decls = parsed
1195
+ standing = decls.reject { |decl| declared.include?(decl.name.absolute!) }
1196
+ next if standing.empty?
1197
+
1198
+ add_parsed_decls(env, buffer, directives, standing)
1199
+ end
1200
+ end
1201
+ end
1202
+
1203
+ # @param env — the environment the project's signatures are already in.
1204
+ def declared_interface_names(env)
1205
+ return Set.new unless env.respond_to?(:interface_decls)
1206
+
1207
+ env.interface_decls.keys.to_set
1208
+ end
1209
+
857
1210
  # Bundled signature sources that SUPPLEMENT a stdlib library's own declarations rather than stand
858
1211
  # alone: each re-opens a class the named `RBS::EnvironmentLoader` library declares with a mixin
859
1212
  # (`CGI::QueryExtension`), a superclass (`Prism::Result`), or an `| ...` overload continuation
@@ -867,6 +1220,10 @@ module Rigor
867
1220
  # `core_overlay/pathname.rbs` also carries an `| ...` continuation but is deliberately NOT listed:
868
1221
  # its base (`Pathname#expand_path`) lives in rbs's `core/pathname.rbs` on every supported rbs
869
1222
  # release (3.10 / 4.0 / 4.1), so the continuation is safe — and applicable — with no library loaded.
1223
+ # `core_overlay/string_io.rbs` re-opens a class the `stringio` library declares yet is NOT listed
1224
+ # either: `RBS::EnvironmentLoader#load` adds that library itself whenever it loads core (rbs 3.7+),
1225
+ # and earlier releases declare `StringIO` in core, so gating would only drop its `Enumerable` mixin
1226
+ # where `StringIO` is present anyway.
870
1227
  #
871
1228
  # Keyed by `data/vendored_gem_sigs/` directory basename.
872
1229
  LIBRARY_SUPPLEMENT_VENDORED_DIRS = {
@@ -883,14 +1240,25 @@ module Rigor
883
1240
  }.freeze
884
1241
  private_constant :LIBRARY_SUPPLEMENT_CORE_OVERLAYS
885
1242
 
1243
+ # `data/core_overlay/` files that fill a hole only one `rbs` release line has, keyed by file basename
1244
+ # to the `rbs` version requirement they load under. Each carries an `| ...` continuation of overloads a
1245
+ # later line declares upstream, so loading it there would add a second copy of them. The env-cache key
1246
+ # already carries `RBS::VERSION` ({Cache::RbsDescriptor.rbs_gem_entry}), so switching lines re-keys.
1247
+ # The requirement is a String, not a `Gem::Requirement`, so the constant stays `Ractor.shareable?`.
1248
+ RBS_LINE_CORE_OVERLAYS = {
1249
+ "hash_rbs3.rbs" => "< 4.0"
1250
+ }.freeze
1251
+ private_constant :RBS_LINE_CORE_OVERLAYS
1252
+
886
1253
  # Adds the Rigor-shipped signature sources to `rbs_loader`: every `data/vendored_gem_sigs/<gem>/`
887
- # directory, then the `data/core_overlay/` files — the overlay LAST so an upstream declaration
888
- # always wins on conflict (these reopenings only fill genuine holes, e.g. `Numeric#to_f`/`to_i`/
889
- # `to_r`, which upstream RBS declares on the concrete subclasses but not on the abstract `Numeric`
890
- # that Rigor's arithmetic-chain widening produces). The overlay is added per-file, not
891
- # per-directory, because the `LIBRARY_SUPPLEMENT_CORE_OVERLAYS` files must be gated individually.
1254
+ # directory, then the `data/core_overlay/` files, which fill genuine holes only (e.g. `Numeric#to_f`/
1255
+ # `to_i`/`to_r`, which upstream RBS declares on the concrete subclasses but not on the abstract `Numeric`
1256
+ # that Rigor's arithmetic-chain widening produces). Load order does not settle a conflict: a method
1257
+ # both sides declare directly raises `DuplicatedMethodDefinitionError` for the whole class. The
1258
+ # overlay is added per-file, not per-directory, because the `LIBRARY_SUPPLEMENT_CORE_OVERLAYS` and
1259
+ # `RBS_LINE_CORE_OVERLAYS` files must be gated individually.
892
1260
  #
893
- # @param loaded_library_names [Set<String>] libraries that actually resolved on this loader.
1261
+ # @param loaded_library_names — libraries that actually resolved on this loader.
894
1262
  def add_bundled_signatures(rbs_loader, loaded_library_names)
895
1263
  vendored_gem_sig_paths.each do |path|
896
1264
  next unless path.directory?
@@ -904,21 +1272,29 @@ module Rigor
904
1272
  dir.children.sort.each do |file|
905
1273
  next unless file.file? && file.extname == ".rbs"
906
1274
  next unless supplement_dependency_loaded?(LIBRARY_SUPPLEMENT_CORE_OVERLAYS, file, loaded_library_names)
1275
+ next unless rbs_line_matches?(file)
907
1276
 
908
1277
  rbs_loader.add(path: file)
909
1278
  end
910
1279
  end
911
1280
  end
912
1281
 
913
- # @param supplements [Hash{String => String}] basename → gating library map.
914
- # @param path [Pathname] the vendored directory or overlay file to test.
915
- # @param loaded_library_names [Set<String>] libraries that actually resolved on this loader.
916
- # @return [Boolean] true when `path` carries no library dependency, or its library loaded.
1282
+ # @param supplements — basename → gating library map.
1283
+ # @param path — the vendored directory or overlay file to test.
1284
+ # @param loaded_library_names — libraries that actually resolved on this loader.
1285
+ # @return true when `path` carries no library dependency, or its library loaded.
917
1286
  def supplement_dependency_loaded?(supplements, path, loaded_library_names)
918
1287
  library = supplements[path.basename.to_s]
919
1288
  library.nil? || loaded_library_names.include?(library)
920
1289
  end
921
1290
 
1291
+ # @param path — a `data/core_overlay/` file.
1292
+ # @return true when `path` is not tied to an `rbs` release line, or the running `rbs` is on its line.
1293
+ def rbs_line_matches?(path)
1294
+ requirement = RBS_LINE_CORE_OVERLAYS[path.basename.to_s]
1295
+ requirement.nil? || Gem::Requirement.new(requirement).satisfied_by?(Gem::Version.new(::RBS::VERSION))
1296
+ end
1297
+
922
1298
  # Rigor-owned per-gem RBS overlays (`data/gem_overlay/<gem>/`), ADR-72. Unlike the unconditional
923
1299
  # `core_overlay`, each gem's overlay is loaded ONLY when that gem is locked in the project's
924
1300
  # Gemfile.lock but ships no RBS of its own — {Environment.for_project} decides eligibility and passes
@@ -929,9 +1305,9 @@ module Rigor
929
1305
  __dir__
930
1306
  ).freeze
931
1307
 
932
- # @param gem_names [Enumerable<String>] overlay-eligible Gemfile.lock gem names (the caller filters
1308
+ # @param gem_names — overlay-eligible Gemfile.lock gem names (the caller filters
933
1309
  # to the `:missing`-coverage, no-conflicting-plugin set).
934
- # @return [Array<Pathname>] the bundled overlay directory for each gem that ships one; empty when
1310
+ # @return the bundled overlay directory for each gem that ships one; empty when
935
1311
  # none match or the overlay root is absent.
936
1312
  def gem_overlay_sig_paths(gem_names)
937
1313
  return [] unless File.directory?(GEM_OVERLAY_SIGS_ROOT)
@@ -942,9 +1318,9 @@ module Rigor
942
1318
  end
943
1319
  end
944
1320
 
945
- # @param path [String, Pathname] typically an entry from an {RbsLoader} instance's
1321
+ # @param path — typically an entry from an {RbsLoader} instance's
946
1322
  # {#signature_paths}.
947
- # @return [Boolean] whether `path` sits under the bundled gem-overlay root — what
1323
+ # @return whether `path` sits under the bundled gem-overlay root — what
948
1324
  # `CheckRules::GEM_OVERLAY_OPEN_RECEIVERS`'s gate consults so that a class name's membership in
949
1325
  # that list alone never grants the open-receiver exemption; the overlay directory that made the
950
1326
  # entry true must have actually loaded THIS run too (issue #632, tracked further by #660).
@@ -982,7 +1358,7 @@ module Rigor
982
1358
  # answer to the very same question, on the class that already owns the overlay's layout
983
1359
  # ({GEM_OVERLAY_SIGS_ROOT}, {.gem_overlay_sig_paths}).
984
1360
  #
985
- # @param signature_paths [Array<String, Pathname>] typically an {RbsLoader} instance's
1361
+ # @param signature_paths — typically an {RbsLoader} instance's
986
1362
  # {#signature_paths}. Takes the whole list rather than one entry so the twin's file set resolves
987
1363
  # once per question — `CheckRules` asks it per `ActiveSupport::Duration` receiver.
988
1364
  def gem_overlay_twin_signatures_loaded?(signature_paths)
@@ -997,7 +1373,7 @@ module Rigor
997
1373
  # id mapping and passes the id; this resolves the id to the engine's own bundled `sig/` and answers
998
1374
  # the filesystem question.
999
1375
  #
1000
- # @param plugin_id [String] a manifest id, e.g. `"activesupport-core-ext"`.
1376
+ # @param plugin_id — a manifest id, e.g. `"activesupport-core-ext"`.
1001
1377
  def bundled_overlay_twin_signatures_loaded?(plugin_id, signature_paths)
1002
1378
  return false if signature_paths.nil? || signature_paths.empty?
1003
1379
 
@@ -1065,23 +1441,27 @@ module Rigor
1065
1441
  end
1066
1442
  end
1067
1443
 
1068
- attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs
1444
+ attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs, :deferred_signature_paths
1069
1445
 
1070
- # @param libraries [Array<String, Symbol>] stdlib library names to load on top of core (e.g.,
1446
+ # @param libraries — stdlib library names to load on top of core (e.g.,
1071
1447
  # `["pathname", "json"]`). Empty by default. Each entry MUST correspond to a directory under the
1072
1448
  # `rbs` gem's `stdlib/` tree; unknown names are silently dropped on environment build (the underlying
1073
1449
  # `RBS::EnvironmentLoader` raises and we fail-soft).
1074
- # @param signature_paths [Array<String, Pathname>] additional directories of `.rbs` files to load
1450
+ # @param signature_paths — additional directories of `.rbs` files to load
1075
1451
  # (typically the project's `sig/` tree). Non-existent or non-directory paths are filtered out at
1076
1452
  # build time so the loader stays robust to fixtures and bare repositories.
1077
- # @param cache_store [Rigor::Cache::Store, nil] the persistent cache the loader threads through to
1453
+ # @param cache_store — the persistent cache the loader threads through to
1078
1454
  # `RbsEnvironment`, `RbsKnownClassNames`, `RbsConstantTable`, `RbsClassAncestorTable`, and
1079
1455
  # `RbsClassTypeParamNames` producers. Pass `nil` (the default) to skip caching; the runner threads
1080
1456
  # its own Store through here when enabled.
1081
- # @param virtual_rbs [Array<[String, String]>] ADR-32 WD4 — `[virtual_filename, rbs_source]` pairs
1457
+ # @param virtual_rbs — >] ADR-32 WD4 — `[virtual_filename, rbs_source]` pairs
1082
1458
  # synthesised from project source by a plugin's `Manifest#source_rbs_synthesizer`. Merged into the
1083
1459
  # env after `signature_paths:` and the vendored stubs. Pass `[]` (the default) when no
1084
1460
  # synthesizer-emitting plugin is loaded.
1461
+ # @param deferred_signature_paths — the subset of `signature_paths:` a bundled plugin
1462
+ # contributed (issue #610): loaded last and allowed to stand down against a colliding generic
1463
+ # arity. Read back by the env-cache producer, which rebuilds the env from the loader's readers on
1464
+ # a miss — a list it cannot read is a stand-down that runs only on a `--no-cache` build.
1085
1465
  def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [],
1086
1466
  deferred_signature_paths: [])
1087
1467
  @libraries = libraries.map(&:to_s).freeze
@@ -1097,8 +1477,8 @@ module Rigor
1097
1477
  # names currently consulted: `:env`, `:env_loaded`, `:env_build_warned`, `:definition_build_warned`,
1098
1478
  # `:definition_build_details`, `:definition_build_reported`, `:definition_build_failures`,
1099
1479
  # `:definition_build_deferred_count`, `:definition_build_deferred_first`,
1100
- # `:definition_build_summary_warned`, `:internal_demand`, `:internal_demand_status`, `:builder`,
1101
- # `:reflection`,
1480
+ # `:definition_build_summary_warned`, `:member_consistency`, `:member_consistency_proven`, `:internal_demand`,
1481
+ # `:internal_demand_status`, `:builder`, `:reflection`,
1102
1482
  # `:instance_definitions_table`, `:singleton_definitions_table`.
1103
1483
  # Constructed via `Hash.new` (NOT a `{ ... }` literal) so Rigor's `HashShape` narrowing doesn't
1104
1484
  # infer a fixed key set from the initial state and fold post-initial slot reads (e.g.
@@ -1127,7 +1507,7 @@ module Rigor
1127
1507
  # the stderr banner) reads it, and a cache HIT reaches it too — the env was built with the file already
1128
1508
  # quarantined, so the condition is invisible in the cached env itself.
1129
1509
  #
1130
- # @return [Array<Array(String, String)>] empty when every `signature_paths:` file parses.
1510
+ # @return empty when every `signature_paths:` file parses.
1131
1511
  def quarantined_signatures
1132
1512
  @state[:quarantined] ||= begin
1133
1513
  parse_quarantined = self.class.quarantined_project_signatures(@signature_paths)
@@ -1135,17 +1515,24 @@ module Rigor
1135
1515
  @state[:env], @signature_paths
1136
1516
  )
1137
1517
  parse_paths = parse_quarantined.to_set { |path, _note| path }
1518
+ # Issue #610 — a deferred file that STOOD DOWN is absent from the env by design, which is exactly
1519
+ # what the collision derivation reads as "quarantined". Derived from the same env, so a cache HIT
1520
+ # excludes it too; {#signature_standdowns} is where it is reported instead.
1521
+ stood_down = self.class.deferred_standdowns(@state[:env], @deferred_signature_paths).to_set(&:first)
1138
1522
  (
1139
1523
  parse_quarantined +
1140
- collision_quarantined.reject { |entry| parse_paths.include?(entry[0]) }
1524
+ collision_quarantined.reject do |entry|
1525
+ parse_paths.include?(entry[0]) || stood_down.include?(entry[0])
1526
+ end
1141
1527
  ).freeze
1142
1528
  end
1143
1529
  end
1144
1530
 
1145
1531
  # Issue #610 — plugin-contributed signature files that stood down against a colliding generic arity,
1146
- # as `[absolute_path, class_name, existing_arity, incoming_arity]`. Empty whenever no plugin sig
1147
- # collides, which is every project that has not run `rbs collection install` for a gem a bundled
1148
- # plugin also declares.
1532
+ # as `[absolute_path, class_name, existing_arity, incoming_arity, existing_file]`. Empty whenever no
1533
+ # plugin sig collides, which is every project that has not run `rbs collection install` for a gem a
1534
+ # bundled plugin also declares. Reported as `rbs.coverage.plugin-signature-stood-down` (`:info`); the
1535
+ # same file MUST NOT also appear in {#quarantined_signatures}.
1149
1536
  def signature_standdowns
1150
1537
  @state[:signature_standdowns] ||= self.class.deferred_standdowns(env, @deferred_signature_paths).freeze
1151
1538
  end
@@ -1185,11 +1572,94 @@ module Rigor
1185
1572
  end
1186
1573
  private :build_project_declared_classes
1187
1574
 
1575
+ # Issue #527 — the class / module names whose PRIMARY declaration lives in the `rbs` gem's own
1576
+ # `core/` or `stdlib/` tree, top-level prefix stripped.
1577
+ #
1578
+ # The distinction it draws is narrower than {#project_declared_classes}'s: not "who wrote this
1579
+ # signature" but "is this Ruby's own documented surface". A core / stdlib signature is the method
1580
+ # set every negative check rule already trusts for a direct receiver of the class, which is what
1581
+ # lets a Ruby-source SUBCLASS of one resolve its inherited calls there (`RbsDispatch`'s
1582
+ # `core_stdlib_ancestor_method`). A GEM's RBS is a different claim — routinely partial, which is
1583
+ # ADR-43's whole reason for declining blanket inherited resolution — so it is deliberately outside
1584
+ # this set even when the gem ships signatures and even when it is one of {DEFAULT_LIBRARIES}
1585
+ # (`prism`, `rbs` are gems whose signatures ship with the gem, not entries of rbs's `stdlib/`).
1586
+ #
1587
+ # Attribution is by buffer NAME, the same mechanism and the same limits as
1588
+ # {#project_declared_classes}: it survives the ADR-54 environment cache, and an old blob's
1589
+ # `<cached>` sentinel lands every class OUTSIDE the set — the feature switches off rather than
1590
+ # widening, which is the direction ADR-5 wants. Memoised per loader; one pass over `class_decls`.
1591
+ def core_stdlib_classes
1592
+ @state[:core_stdlib_classes] ||= build_core_stdlib_classes
1593
+ end
1594
+
1595
+ def build_core_stdlib_classes
1596
+ environment = env
1597
+ return Set.new.freeze if environment.nil? || CORE_STDLIB_ROOTS.empty?
1598
+
1599
+ names = environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
1600
+ decl = self.class.primary_decl_for(entry)
1601
+ next if decl.nil?
1602
+
1603
+ path = self.class.declaration_buffer_name(decl)
1604
+ next if path.nil?
1605
+
1606
+ acc << rbs_name.to_s.delete_prefix("::") if CORE_STDLIB_ROOTS.any? { |root| path.start_with?(root) }
1607
+ end
1608
+ names.freeze
1609
+ rescue ::RBS::BaseError
1610
+ Set.new.freeze
1611
+ end
1612
+ private :build_core_stdlib_classes
1613
+
1614
+ # True when `class_name` is declared by Ruby core or a stdlib library (see {#core_stdlib_classes}).
1615
+ def core_or_stdlib_class?(class_name)
1616
+ core_stdlib_classes.include?(class_name.to_s.delete_prefix("::"))
1617
+ end
1618
+
1188
1619
  # True when `class_name`'s RBS declaration is one the project wrote (see {#project_declared_classes}).
1189
1620
  def project_declared_class?(class_name)
1190
1621
  project_declared_classes.include?(class_name.to_s.delete_prefix("::"))
1191
1622
  end
1192
1623
 
1624
+ # Issue #1362 — the translated type of the RBS declaration of the global variable `name` (`:$stdout`, declared
1625
+ # `IO` by the core signatures), or nil when no loaded signature declares it. Translated without an alias
1626
+ # expander, as {#constant_type} translates a constant, so `$DEBUG: boolish` reads `Dynamic[top]` rather than
1627
+ # `top`.
1628
+ def global_type(name)
1629
+ global_declarations[name.to_sym]&.first
1630
+ end
1631
+
1632
+ # True when `name`'s RBS declaration lives in the `rbs` gem's own `core/` or `stdlib/` tree: a global the
1633
+ # interpreter or a default library sets. Attributed by buffer name, as {#core_or_stdlib_class?} is, with the
1634
+ # same limit: an environment blob without buffer names answers false for every global.
1635
+ def core_or_stdlib_global?(name)
1636
+ global_declarations[name.to_sym]&.last || false
1637
+ end
1638
+
1639
+ # `name => [type, core_or_stdlib]` for every global the environment declares; a declaration whose type
1640
+ # translates to `bot` is left out. Memoised per loader: the core signatures declare about fifty.
1641
+ def global_declarations
1642
+ @state[:global_declarations] ||= build_global_declarations
1643
+ end
1644
+ private :global_declarations
1645
+
1646
+ def build_global_declarations
1647
+ environment = env
1648
+ return {}.freeze if environment.nil?
1649
+
1650
+ table = environment.global_decls.each_with_object({}) do |(name, entry), acc|
1651
+ type = Inference::RbsTypeTranslator.translate(entry.decl.type)
1652
+ next if type.is_a?(Type::Bot)
1653
+
1654
+ path = self.class.declaration_buffer_name(entry.decl)
1655
+ acc[name.to_sym] = [type, !path.nil? && CORE_STDLIB_ROOTS.any? { |root| path.start_with?(root) }].freeze
1656
+ end
1657
+ table.freeze
1658
+ rescue ::RBS::BaseError
1659
+ {}.freeze
1660
+ end
1661
+ private :build_global_declarations
1662
+
1193
1663
  # The total RBS-environment build failure captured this run, or nil when the env built. Unlike
1194
1664
  # {#quarantined_signatures} — which the env survives, one file lighter, and which is re-derived so a
1195
1665
  # cache HIT reports it too — a total failure collapses the WHOLE env to nil. Project-vs-bundled
@@ -1199,7 +1669,7 @@ module Rigor
1199
1669
  # persisted, so every run re-attempts and re-raises), so this is captured directly in {#env}'s rescue
1200
1670
  # rather than re-derived. Forcing `env` (any query does) populates it.
1201
1671
  #
1202
- # @return [Array(String, String, Array<String>), nil] `[error_class_name, first_error_line,
1672
+ # @return `[error_class_name, first_error_line,
1203
1673
  # conflicting_buffer_names]`, or nil when the environment built successfully.
1204
1674
  def env_build_failure
1205
1675
  env unless @state[:env_loaded]
@@ -1223,9 +1693,10 @@ module Rigor
1223
1693
  # definition-build failure leaves no trace in the env at all — the env is fine; it is the BUILD over it
1224
1694
  # that raised — so the rescue is the only place that ever knows.
1225
1695
  #
1226
- # @return [Array<Array(String, String, String, Array<String>)>] `[class_name, error_class_name,
1227
- # first_error_line, conflicting_buffer_names]`, one per class, in first-failure order. Empty for a
1228
- # healthy sig set, which is the common case.
1696
+ # @return `[class_name, error_class_name, member, conflicting_buffer_names,
1697
+ # unresolved_type_name_detail]`, one per class, in first-failure order (issue #997 added the fifth
1698
+ # slot; see {#unresolved_type_name_detail} — `nil` there for every failure kind but an unresolvable
1699
+ # type name). Empty for a healthy sig set, which is the common case.
1229
1700
  def definition_build_failures
1230
1701
  (@state[:definition_build_failures] || []).dup.freeze
1231
1702
  end
@@ -1237,7 +1708,7 @@ module Rigor
1237
1708
  # the {#quarantined_signatures} trick — so a cache HIT, which never runs the build, reports the same
1238
1709
  # condition: the marshalled env simply lacks the dropped buffers.
1239
1710
  #
1240
- # @return [Array<String>] virtual buffer names (source-file paths) whose contribution was dropped.
1711
+ # @return virtual buffer names (source-file paths) whose contribution was dropped.
1241
1712
  def virtual_rbs_collision_quarantined
1242
1713
  @state[:virtual_rbs_collisions] ||= begin
1243
1714
  built = @state[:env]
@@ -1252,6 +1723,29 @@ module Rigor
1252
1723
  end
1253
1724
  end
1254
1725
 
1726
+ # Issue #1075 / ADR-112 WD5 — one {MemberConsistency::Record} per `(class, method, kind)` the project's
1727
+ # own `signature_paths:` and an inline (rbs-inline) source both declare, saying which side bound and
1728
+ # why, plus one per `rigor:v1:return:` / `rigor:v1:param:` refinement that exceeds its member's own
1729
+ # declared type. The rest of the file's annotations still bind either way, and the class keeps its
1730
+ # method surface instead of failing its definition build.
1731
+ #
1732
+ # The sibling of {#virtual_rbs_collision_quarantined} one granularity finer: that one names a whole
1733
+ # inline FILE dropped for a class-level duplicate, this one names a single member. Both are derived
1734
+ # rather than recorded, so a cache HIT — where no build ran — reports the same condition; here the
1735
+ # derivation reads the loader's inputs rather than the built env, because a stripped member leaves no
1736
+ # trace in the env at all.
1737
+ #
1738
+ # @return the records, sorted. Empty (and free) whenever the project contributes no inline RBS.
1739
+ #
1740
+ # @param proof — a {MemberConsistency::RbsProof} over this loader, without which no pair is reported
1741
+ # as a contradiction (only the severity of a row depends on it, never which declaration binds).
1742
+ def member_consistency(proof: nil)
1743
+ key = proof ? :member_consistency_proven : :member_consistency
1744
+ @state[key] ||= self.class.member_consistency_for(
1745
+ self.class.project_sig_files(@signature_paths), @virtual_rbs, proof: proof
1746
+ ).records
1747
+ end
1748
+
1255
1749
  # The referenced-but-undeclared types {.stub_missing_referenced_types} stubbed so the project classes
1256
1750
  # that mention them could build (e.g. an unavailable `DRb::DRbServer`, or a stale
1257
1751
  # `Textbringer::EditorError`). Recovered off the built env like {#synthesized_namespaces}, so it
@@ -1415,7 +1909,7 @@ module Rigor
1415
1909
  {}.freeze
1416
1910
  end
1417
1911
 
1418
- # @return [RBS::Definition, nil] the resolved instance definition for `class_name`, or nil when the
1912
+ # @return the resolved instance definition for `class_name`, or nil when the
1419
1913
  # class is unknown or its definition cannot be built (RBS may raise on broken hierarchies; we
1420
1914
  # fail-soft and return nil so the caller can fall back).
1421
1915
  #
@@ -1441,7 +1935,6 @@ module Rigor
1441
1935
  definition
1442
1936
  end
1443
1937
 
1444
- # @return [RBS::Definition::Method, nil]
1445
1938
  def instance_method(class_name:, method_name:)
1446
1939
  definition = instance_definition(class_name)
1447
1940
  return nil unless definition
@@ -1449,7 +1942,7 @@ module Rigor
1449
1942
  definition.methods[method_name.to_sym]
1450
1943
  end
1451
1944
 
1452
- # @return [Array<Symbol>, nil] every instance-method name on `class_name` — own, inherited, and
1945
+ # @return every instance-method name on `class_name` — own, inherited, and
1453
1946
  # included — as resolved by `RBS::DefinitionBuilder`. Returns `nil` (NOT `[]`) when the class
1454
1947
  # definition cannot be built so callers can tell "no methods" apart from "unknown class". Used by the
1455
1948
  # `rigor:v1:conforms-to` presence check ({Rigor::RbsExtended::ConformanceChecker}).
@@ -1460,7 +1953,7 @@ module Rigor
1460
1953
  definition.methods.keys
1461
1954
  end
1462
1955
 
1463
- # @return [RBS::Definition, nil] the built definition for the RBS interface `interface_name`
1956
+ # @return the built definition for the RBS interface `interface_name`
1464
1957
  # (`_RewindableStream`), whose `.methods` are the required members (including interface-ancestor
1465
1958
  # members). Returns `nil` when the name does not resolve to a loaded interface (a typo, or the
1466
1959
  # defining library / sig set is not on the load path). Fail-soft on RBS build errors.
@@ -1475,15 +1968,15 @@ module Rigor
1475
1968
  nil
1476
1969
  end
1477
1970
 
1478
- # @return [Array<Symbol>, nil] every method name required by the RBS interface `interface_name`, or nil
1971
+ # @return every method name required by the RBS interface `interface_name`, or nil
1479
1972
  # when it does not resolve. Thin accessor over {#interface_definition} for the presence check.
1480
1973
  def interface_method_names(interface_name)
1481
1974
  interface_definition(interface_name)&.methods&.keys
1482
1975
  end
1483
1976
 
1484
- # @param rbs_alias [RBS::Types::Alias] a type-alias reference (`string`, `int`, `range[int?]`, …)
1977
+ # @param rbs_alias — a type-alias reference (`string`, `int`, `range[int?]`, …)
1485
1978
  # appearing in a method signature.
1486
- # @return [RBS::Types::t, nil] the alias's aliased type one level out, with type arguments substituted
1979
+ # @return the alias's aliased type one level out, with type arguments substituted
1487
1980
  # for a generic alias (`string` → `::String | ::_ToStr`; `range[int?]` → `::Range[int?] |
1488
1981
  # ::_Range[int?]`), or nil for an unresolved name. Lets a caller see through the alias that
1489
1982
  # {Inference::RbsTypeTranslator} otherwise degrades to `untyped`, which is why an interface/alias
@@ -1508,7 +2001,7 @@ module Rigor
1508
2001
  nil
1509
2002
  end
1510
2003
 
1511
- # @return [RBS::Definition, nil] the resolved singleton (class object) definition for `class_name`. The
2004
+ # @return the resolved singleton (class object) definition for `class_name`. The
1512
2005
  # methods on this definition are the *class methods* of `class_name`, including those inherited from
1513
2006
  # `Class` and `Module` for class types. Returns nil for unknown names and on RBS build errors
1514
2007
  # (fail-soft).
@@ -1529,7 +2022,37 @@ module Rigor
1529
2022
  definition
1530
2023
  end
1531
2024
 
1532
- # @return [RBS::Definition::Method, nil] the class method on `class_name`. For example,
2025
+ # Issue #915 — the MODULE names in `class_name`'s singleton ancestry, as RBS declares it. A
2026
+ # `class C; extend M; end` written in a project's own `sig/` puts `M` there and nothing else in the
2027
+ # environment surface exposes it: {#ancestor_names_for} answers the instance side only.
2028
+ #
2029
+ # Modules only, deliberately. `RBS::DefinitionBuilder#build_singleton` mixes the extended modules in
2030
+ # with the singleton's class chain (`Class`, `Module`, `Object`, `BasicObject`), and the classes there
2031
+ # are already what the `Class` approximation in `Inference::Narrowing` reasons through. The modules
2032
+ # that survive the filter — the declared `extend` targets, plus `Kernel` through `Object` — are each a
2033
+ # true answer to `C.is_a?(M)`.
2034
+ #
2035
+ # Marked as an internal demand for the same reason {#ancestor_names_for} is (issue #696): asking
2036
+ # whether a module sits in an ancestry is not asking whether the class's method surface resolves, and
2037
+ # letting it report would make the reported class list depend on the cache state.
2038
+ #
2039
+ # @return `::`-stripped module names, or `[]` for an unknown class and on any RBS build error.
2040
+ def singleton_extended_module_names(class_name)
2041
+ key = class_name.to_s.delete_prefix("::")
2042
+ during_internal_demand do
2043
+ definition = singleton_definition(key)
2044
+ next [].freeze if definition.nil?
2045
+
2046
+ definition.ancestors.ancestors.filter_map do |ancestor|
2047
+ name = ancestor.name.to_s.delete_prefix("::")
2048
+ name if ancestor.is_a?(::RBS::Definition::Ancestor::Instance) && rbs_module?(name)
2049
+ end.uniq.freeze
2050
+ end
2051
+ rescue ::RBS::BaseError, StandardError
2052
+ [].freeze
2053
+ end
2054
+
2055
+ # @return the class method on `class_name`. For example,
1533
2056
  # `singleton_method(class_name: "Integer", method_name: :sqrt)` returns the definition for
1534
2057
  # `Integer.sqrt`, while `singleton_method(class_name: "Foo", method_name: :new)` returns Class#new
1535
2058
  # for any class type.
@@ -1553,18 +2076,29 @@ module Rigor
1553
2076
  # process) through {Cache::RbsClassTypeParamNames.fetch} and answers point lookups from it. Cold runs
1554
2077
  # build the table once and persist it; warm runs (and a separate loader sharing the same Store) skip
1555
2078
  # the env walk entirely.
2079
+ #
2080
+ # #775 — memoised per class name and answered as a frozen list. `RBS::Definition#type_params` is not
2081
+ # an accessor: it re-derives the names through the entry's type-param declarations (a substitution
2082
+ # per call), and generic dispatch asks for the receiver class's names on every call (~110k times on
2083
+ # the lib self-check, the largest remaining rbs-side allocation). The env is immutable for the
2084
+ # loader's lifetime, so the answer never changes; every consumer only reads it.
1556
2085
  def class_type_param_names(class_name)
1557
- if cache_store
1558
- key = class_name.to_s.delete_prefix("::")
1559
- return type_param_names_table.fetch(key, []).dup
1560
- end
1561
-
1562
- definition = instance_definition(class_name)
1563
- return [] unless definition
2086
+ memo = (@state[:type_param_names] ||= {})
2087
+ key = class_name.to_s
2088
+ return memo[key] if memo.key?(key)
1564
2089
 
1565
- definition.type_params.dup
2090
+ memo[key] =
2091
+ if cache_store
2092
+ type_param_names_table.fetch(key.delete_prefix("::"), NO_TYPE_PARAMS).freeze
2093
+ else
2094
+ definition = instance_definition(class_name)
2095
+ definition ? definition.type_params.freeze : NO_TYPE_PARAMS
2096
+ end
1566
2097
  end
1567
2098
 
2099
+ NO_TYPE_PARAMS = [].freeze
2100
+ private_constant :NO_TYPE_PARAMS
2101
+
1568
2102
  def class_ordering(lhs, rhs)
1569
2103
  @hierarchy.class_ordering(lhs, rhs)
1570
2104
  end
@@ -1595,7 +2129,7 @@ module Rigor
1595
2129
  # way, and both yield `[]` for an unknown or unbuildable class. Pinned by spec across all three cache
1596
2130
  # states.
1597
2131
  #
1598
- # @return [Array<String>] `::`-stripped ancestor names, or `[]` for an unknown or unbuildable class.
2132
+ # @return `::`-stripped ancestor names, or `[]` for an unknown or unbuildable class.
1599
2133
  def ancestor_names_for(class_name)
1600
2134
  key = class_name.to_s.delete_prefix("::")
1601
2135
  during_internal_demand do
@@ -1610,7 +2144,7 @@ module Rigor
1610
2144
  [].freeze
1611
2145
  end
1612
2146
 
1613
- # @return [Array<String>] every RBS-declared constant name (top-level prefixed, e.g., `"::Math::PI"`)
2147
+ # @return every RBS-declared constant name (top-level prefixed, e.g., `"::Math::PI"`)
1614
2148
  # currently loaded into the environment. Used by the cache producer that materialises the
1615
2149
  # constant-type table; ordinary callers should keep using {#constant_type} for point lookups.
1616
2150
  def constant_names
@@ -1694,11 +2228,21 @@ module Rigor
1694
2228
  # Building it digests every `.rbs` file under `signature_paths` + the vendored gem sigs, and the
1695
2229
  # result is identical across producers, so one build is memoised per loader (on `@state`, alongside
1696
2230
  # `:env` — the env itself is loader-lifetime-memoised, so this adds no new staleness class).
2231
+ #
2232
+ # Issue #1014 — nil when the engine's source cannot be digested ({Cache::EngineSource::Unavailable});
2233
+ # {Cache::RbsCacheProducer.fetch} then computes uncached. Memoised through `key?` rather than `||=` so
2234
+ # that answer is reached once: rebuilding it would re-digest the whole signature tree per producer to
2235
+ # arrive at the same nil.
1697
2236
  def rbs_cache_descriptor
1698
- @state[:rbs_cache_descriptor] ||= begin
1699
- require_relative "../cache/rbs_descriptor"
1700
- Cache::RbsDescriptor.build(self)
1701
- end
2237
+ return @state[:rbs_cache_descriptor] if @state.key?(:rbs_cache_descriptor)
2238
+
2239
+ require_relative "../cache/rbs_descriptor"
2240
+ @state[:rbs_cache_descriptor] =
2241
+ begin
2242
+ Cache::RbsDescriptor.build(self)
2243
+ rescue Cache::EngineSource::Unavailable
2244
+ nil
2245
+ end
1702
2246
  end
1703
2247
 
1704
2248
  # ADR-15 Phase 2b — return the loader's read-only query surface as a frozen, `Ractor.shareable?`
@@ -2066,7 +2610,60 @@ module Rigor
2066
2610
  return if details.key?(key)
2067
2611
 
2068
2612
  details[key] = [key, error.class.name.to_s, definition_build_member(error),
2069
- definition_build_conflict_buffers(error)].freeze
2613
+ definition_build_conflict_buffers(error), unresolved_type_name_detail(error)].freeze
2614
+ end
2615
+
2616
+ # Issue #997 — the `RBS::NoTypeFoundError` twin of {#definition_build_member}: `nil` for every other
2617
+ # error class, so the cached tuple gains a populated fifth slot only for the failure kind it
2618
+ # distinguishes — "one declaration names a type that does not exist" from "two declarations collide"
2619
+ # ({DiagnosticAggregator#definition_build_advice}, which folds this in).
2620
+ #
2621
+ # Computed once, at RAISE time, and frozen into the cached detail rather than re-derived when a warm
2622
+ # run replays the tuple from {IncrementalSnapshot} — there is no live `RBS::NoTypeFoundError` to
2623
+ # re-inspect on that path, only the plain strings this method already reduced it to.
2624
+ #
2625
+ # `[token, location, refinement_hint]`:
2626
+ # - `token` is the unresolved name exactly as `RBS::NoTypeFoundError#type_name` carries it — the ONLY
2627
+ # name that ever reaches RBS. A hyphenated payload (`finite-float`) is truncated to its first
2628
+ # segment by rbs-inline's own tolerant type parse (`RBS::Parser.parse_type(..., require_eof: false)`
2629
+ # stops at the first complete type and silently discards the rest), so `-float` never becomes a
2630
+ # token RBS itself can name or complain about.
2631
+ # - `location` is `error.location.to_s` verbatim (`"virtual:rbs-inline:<path>:line:col...line:col"`)
2632
+ # — the position the error already carries, per issue #997, named rather than re-derived. It is a
2633
+ # position in the SYNTHESIZED buffer for an inline annotation, not necessarily the `.rb` line the
2634
+ # author wrote: {Effects::InlineAnchor} maps a known `%a{...}` spelling back by ordinal, which does
2635
+ # not apply to an arbitrary broken type token, so no such mapping is attempted here.
2636
+ # - `refinement_hint` is the full compound name (`"finite-float"`) when the SAME synthesized buffer —
2637
+ # rbs-inline re-emits the author's original `# @rbs name: TYPE` comment verbatim above the member it
2638
+ # generates — carries `token` immediately followed by a hyphenated continuation that is ITSELF a
2639
+ # registered {Builtins::ImportedRefinements} name; `nil` otherwise. Exact-match only: a near-miss
2640
+ # compound name says nothing, so a coincidental `token-suffix` that is not a real refinement can
2641
+ # never manufacture a misleading "did you mean" (false positives outrank worst-case reading).
2642
+ def unresolved_type_name_detail(error)
2643
+ return nil unless error.is_a?(::RBS::NoTypeFoundError)
2644
+
2645
+ token = error.type_name.to_s.delete_prefix("::")
2646
+ [token, error.location&.to_s, refinement_hint_for(token, error)].freeze
2647
+ rescue ::RBS::BaseError, StandardError
2648
+ nil
2649
+ end
2650
+
2651
+ # The buffer rbs-inline synthesized still carries the author's own comment line verbatim (the
2652
+ # "annotation echo" {Effects::InlineAnchor} documents), so a hyphenated refinement name truncated out
2653
+ # of the parsed type ({#unresolved_type_name_detail}) is still readable a few characters further along
2654
+ # the SAME text the error's own location already points into.
2655
+ def refinement_hint_for(token, error)
2656
+ return nil if token.empty?
2657
+
2658
+ content = error.location&.buffer&.content
2659
+ return nil if content.nil?
2660
+
2661
+ candidate = content[/\b#{Regexp.escape(token)}-[A-Za-z0-9][\w-]*/]
2662
+ return nil if candidate.nil?
2663
+
2664
+ Builtins::ImportedRefinements.known?(candidate) ? candidate : nil
2665
+ rescue ::RBS::BaseError, StandardError
2666
+ nil
2070
2667
  end
2071
2668
 
2072
2669
  # Issue #696 — the REPORTING half: promote a remembered detail into {#definition_build_failures}
@@ -2276,8 +2873,6 @@ module Rigor
2276
2873
  # `#method_name`; `UnknownMethodAliasError` and `NoSuperclassFoundError` have `#type_name`;
2277
2874
  # `SuperclassMismatchError` has `#name`. `RecursiveAncestorError` has none of them and yields nil, and
2278
2875
  # the diagnostic then omits the clause rather than inventing one.
2279
- #
2280
- # @return [String, nil]
2281
2876
  def definition_build_member(error)
2282
2877
  return error.qualified_method_name.to_s if error.respond_to?(:qualified_method_name)
2283
2878