rigortype 0.3.9 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require_relative "../cache/value_digest"
5
+
6
+ module Rigor
7
+ class Environment
8
+ # ADR-32 WD5 — one plugin's `source_rbs_synthesizer` output for one source file, cached per (file content,
9
+ # plugin entry, engine source). Two callers read it, and sharing the function is the point:
10
+ #
11
+ # - {Environment.collect_virtual_rbs} hands each output to the loader as a `virtual:` buffer;
12
+ # - {.digest} reduces every loaded synthesizer's output for a file to one comparable value, which the
13
+ # incremental session (ADR-89 WD1, issue #1536) keeps on the file's seed bundle and compares across an
14
+ # edit.
15
+ #
16
+ # Because the digest reads the very function (and the very cache entries) the loader is fed from, "the
17
+ # digest did not move" means "the loader read the same bytes for this file", not "some model of what
18
+ # the plugin reads did not move". That is why it is taken from the output rather than from the file's
19
+ # comment lines: rbs-inline binds an annotation by adjacency, so a plain comment or a blank line can move
20
+ # one to another declaration, and in an annotated file every `def`, `attr`, constant and mixin gets a
21
+ # skeleton whether or not it carries an annotation of its own.
22
+ module SourceRbsSynthesis
23
+ PRODUCER_ID = "plugin.source_rbs_synthesizer"
24
+
25
+ # The digest of a file no loaded synthesizer contributes anything for — the answer for every file of a
26
+ # project without an annotation, and for every file when no synthesizer is loaded at all. Not a hex
27
+ # digest, so it can never collide with one.
28
+ NO_CONTRIBUTION = "none"
29
+
30
+ module_function
31
+
32
+ # The cache stores the empty string `""` as the "no contribution" sentinel because `Cache::Store`
33
+ # treats `nil` as a cache miss. Error tuples are stored as the canonical `[:error, message_string]`
34
+ # Array so the same wrapper short-circuits subsequent runs against unchanged broken input.
35
+ def output_for(plugin, callable, path, cache_store)
36
+ return invoke_safely(callable, path) if cache_store.nil?
37
+ return invoke_safely(callable, path) unless File.file?(path)
38
+
39
+ descriptor = cache_descriptor(plugin, path)
40
+ return invoke_safely(callable, path) if descriptor.nil?
41
+
42
+ cache_store.fetch_or_compute(
43
+ producer_id: PRODUCER_ID,
44
+ params: {},
45
+ descriptor: descriptor,
46
+ generation_cap: generation_cap
47
+ ) { invoke_safely(callable, path) || "" }
48
+ end
49
+
50
+ # Issue #1536 — every synthesizer's output for `path`, reduced to one value: {NO_CONTRIBUTION} when none
51
+ # of them contributes anything, a SHA-256 hex digest of each plugin's id and {.contribution} otherwise,
52
+ # and nil when an output could not be read at all.
53
+ #
54
+ # nil means "unknown", and a caller must treat it as moved. Every failure is per file: one plugin that
55
+ # cannot answer for one file leaves every other file's digest intact.
56
+ #
57
+ # @param synthesizers — `Plugin::Registry#source_rbs_synthesizers` pairs.
58
+ def digest(synthesizers, path, cache_store)
59
+ sha = Digest::SHA256.new
60
+ contributed = false
61
+ synthesizers.each do |plugin, callable|
62
+ tag, text = contribution(output_for(plugin, callable, path, cache_store))
63
+ next if tag.nil?
64
+
65
+ contributed = true
66
+ sha << plugin.manifest.id.to_s.b << "\x00" << tag << "\x00" << text.to_s.b << "\x01"
67
+ end
68
+ contributed ? sha.hexdigest : NO_CONTRIBUTION
69
+ rescue StandardError
70
+ nil
71
+ end
72
+
73
+ # What one output puts into the environment, as a `[tag, text]` pair, or nil for nothing: exactly the
74
+ # RBS {Environment.interpret_synthesizer_outcome} hands the loader. The ADR-32 WD6 / WD12 notices are left
75
+ # out. They reach only the run-level `source-rbs-*` rows, which every run regenerates and the per-file
76
+ # cache never serves, and they quote line numbers, so a line shift in a file carrying a malformed `#:`
77
+ # would otherwise read as a moved contribution. A failed synthesis contributes nothing to the loader but
78
+ # is kept as one stable value of its own, so a file flipping between "no annotation" and "broken
79
+ # annotation" still reads as moved. Any shape the contract does not name is digested whole, by value: the
80
+ # output is computed on one run and served from its cache entry on the next, and a digest of its
81
+ # `Marshal.dump` bytes can differ between the two (issue #1574).
82
+ #
83
+ # The RBS text itself is digested whole, comments included. RBS reads a `# resolve-type-names: false`
84
+ # magic comment at the start of a buffer, rbs-inline copies a `.rb` file's first comment line there, and
85
+ # a string-literal type may span lines that start with `#`, so no line-based stripping is sound (#1549).
86
+ def contribution(output)
87
+ return nil if output.nil? || output == ""
88
+ return ["rbs", output] if output.is_a?(String)
89
+ return ["raw", Cache::ValueDigest.hexdigest(output)] unless output.is_a?(Array)
90
+
91
+ case output[0]
92
+ when :error then ["error", nil]
93
+ when :ok then contribution(output[1])
94
+ else ["raw", Cache::ValueDigest.hexdigest(output)]
95
+ end
96
+ end
97
+
98
+ # One entry per (plugin, source file), all of them live for as long as the file is in the project — a
99
+ # generation count says nothing about staleness here, so this producer declares itself out of
100
+ # `Cache::Store#evict!`'s compaction pass and is bounded only by the size-based LRU pass. A method
101
+ # rather than a constant: `Cache::Store` is not loaded yet when this file is.
102
+ def generation_cap
103
+ Cache::Store::UNBOUNDED_GENERATIONS
104
+ end
105
+
106
+ # The key composes the file's content SHA with the plugin's `PluginEntry` (id + version + config_hash),
107
+ # so a config change or a content change invalidates the entry, and with the engine's source identity
108
+ # (issue #1009): a plugin's manifest version does not move when a checkout edits its synthesizer, and
109
+ # a stale string here also keeps the `rbs.virtual_rbs` env key warm, so the new build's rules would
110
+ # read the old build's RBS.
111
+ def cache_descriptor(plugin, path)
112
+ Cache::Descriptor.new(
113
+ files: [Cache::Descriptor::FileEntry.new(
114
+ path: path.to_s,
115
+ comparator: :digest,
116
+ value: input_digest(path)
117
+ )],
118
+ plugins: [plugin.plugin_entry],
119
+ configs: Cache::EngineSource.key_config_entries
120
+ )
121
+ rescue Cache::EngineSource::Unavailable
122
+ # An engine that cannot be identified must not be keyed by its inputs alone (issue #1009): nil runs the
123
+ # synthesizer uncached rather than serving an entry another build may have written.
124
+ nil
125
+ end
126
+
127
+ def input_digest(path)
128
+ Digest::SHA256.hexdigest(File.binread(path))
129
+ rescue ::SystemCallError
130
+ # Unreadable file → key on the path alone; the synthesizer's File.file?/File.read will see the same
131
+ # failure and return nil.
132
+ Digest::SHA256.hexdigest(path.to_s)
133
+ end
134
+
135
+ def invoke_safely(callable, path)
136
+ callable.call(path.to_s)
137
+ rescue StandardError
138
+ # WD6 fail-soft — a synthesizer that raises does NOT crash analysis. Unlike the `[:error, msg]`
139
+ # return path (which the runner surfaces as `source-rbs-synthesis-failed`), an unhandled raise is
140
+ # swallowed silently; the unexamined-raise channel is deliberately silent per WD6.
141
+ nil
142
+ end
143
+
144
+ private_class_method :contribution, :generation_cap, :cache_descriptor, :input_digest, :invoke_safely
145
+ end
146
+ end
147
+ end
@@ -15,7 +15,9 @@ require_relative "environment/bundle_sig_discovery"
15
15
  require_relative "environment/lockfile_resolver"
16
16
  require_relative "environment/installed_gem_set"
17
17
  require_relative "environment/rbs_collection_discovery"
18
+ require_relative "environment/source_rbs_synthesis"
18
19
  require_relative "plugin/isolation"
20
+ require_relative "cache/engine_source"
19
21
  require_relative "environment/rbs_coverage_report"
20
22
  require_relative "inference/synthetic_method_index"
21
23
  require_relative "inference/project_patched_methods"
@@ -273,8 +275,8 @@ module Rigor
273
275
  bundler_lockfile: nil,
274
276
  rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
275
277
  synthetic_method_index: nil, project_patched_methods: nil,
276
- source_files: [])
277
- resolved_paths = signature_paths || default_signature_paths(root)
278
+ source_files: [], locked_gems: nil)
279
+ resolved_paths = project_signature_roots(signature_paths, root: root)
278
280
  # O4 MVP — append per-gem `sig/` directories discovered under the target project's bundler install
279
281
  # root. Empty array when neither an explicit path nor auto-detection finds a bundle. Order: user
280
282
  # `signature_paths:` win first (semantic precedence inside `RbsLoader.build_env_for`); gem-shipped
@@ -284,7 +286,10 @@ module Rigor
284
286
  # to the project root), use the locked gem set to filter the discovered `sig/` directories. Stale
285
287
  # gems in the bundle install tree (out-of-band installs, version drift after a `bundle update`) are
286
288
  # silently dropped so only gems the project actually declares contribute RBS.
287
- locked = LockfileResolver.locked_gems(
289
+ #
290
+ # #1064 — `locked_gems:` is that same map resolved by the caller. The Ractor pool passes it because
291
+ # Bundler's parser cannot run in a non-main Ractor; every other caller leaves it nil and resolves here.
292
+ locked = locked_gems || LockfileResolver.locked_gems(
288
293
  lockfile_path: bundler_lockfile,
289
294
  project_root: root,
290
295
  auto_detect: bundler_auto_detect
@@ -358,13 +363,14 @@ module Rigor
358
363
  # the bundle walk and `rbs collection install` all stay authoritative.
359
364
  deferred_signature_paths: plugin_sig_paths
360
365
  )
361
- # Issue #824 / ADR-32 WD13 — one `:info` per inline member the loader stood down against the
362
- # project's own `sig/`. Recorded HERE because this is where the reporter is: the run's
363
- # `source_rbs_synthesis` stream already carries WD12's "parsed but not honoured" rows, drains out of
364
- # pool workers, and is regenerated (never cached) on every run, so the member-level stand-down needs
365
- # no plumbing of its own. Costs nothing for a project with no inline RBS: the loader's reader
366
- # short-circuits on an empty `virtual_rbs`.
367
- record_inline_member_standdowns(loader, source_rbs_synthesis_reporter, root)
366
+ # Issues #824 / #1075 — the rows ADR-112 WD5's consistency rule reports for members the project's
367
+ # own `sig/` and an inline source both declare: an error per contradiction (or per refinement that
368
+ # exceeds its own declared type), an `:info` per inline member dropped undecided. Recorded HERE
369
+ # because this is where the reporter is: the run's `source_rbs_synthesis` stream already carries
370
+ # WD12's "parsed but not honoured" rows, drains out of pool workers, and is regenerated (never cached)
371
+ # on every run, so the member-level outcome needs no plumbing of its own. Costs nothing for a project
372
+ # with no inline RBS: the loader's reader short-circuits on an empty `virtual_rbs`.
373
+ record_member_consistency(loader, source_rbs_synthesis_reporter, root)
368
374
  # ADR-20 slice 2c + 2e — seed hkt_registry with the bundled builtins. The Environment's
369
375
  # `#hkt_registry` getter then LAZILY merges in the RBS env scan on first call so fast paths that
370
376
  # don't consult HKT (e.g. `rigor check --cache-stats --no-stats`) don't pay the eager env-build cost
@@ -385,6 +391,13 @@ module Rigor
385
391
  )
386
392
  end
387
393
 
394
+ # The project-owned `sig/` roots a run reads: the explicit `signature_paths:` when given, else the
395
+ # auto-detected `<root>/sig` (or none). Public so {Cache::IncrementalSnapshot} digests exactly the
396
+ # directories the environment loads, including the auto-detected one a `nil` `signature_paths:` hides.
397
+ def project_signature_roots(signature_paths, root: Dir.pwd)
398
+ signature_paths || default_signature_paths(root)
399
+ end
400
+
388
401
  private
389
402
 
390
403
  def default_signature_paths(root)
@@ -491,8 +504,9 @@ module Rigor
491
504
  # empty and this method short-circuits to `[]` without walking the file list.
492
505
  #
493
506
  # WD5 — when `cache_store` is supplied, each (file, plugin) synthesizer call is memoised through
494
- # `Cache::Store`. The cache key composes the file's content SHA with the plugin's `PluginEntry` (id +
495
- # version + config_hash) so a config change or content change invalidates the entry automatically.
507
+ # `Cache::Store` ({SourceRbsSynthesis.output_for}, which owns the key). The incremental session's
508
+ # per-file digest (issue #1536) reads the same function, so the two can never disagree on what a file
509
+ # contributed.
496
510
  def collect_virtual_rbs(plugin_registry, source_files, cache_store, reporter)
497
511
  return [] if plugin_registry.nil?
498
512
 
@@ -503,7 +517,7 @@ module Rigor
503
517
  result = []
504
518
  source_files.each do |path|
505
519
  synthesizers.each do |plugin, callable|
506
- outcome = synthesizer_output_for(plugin, callable, path, cache_store)
520
+ outcome = SourceRbsSynthesis.output_for(plugin, callable, path, cache_store)
507
521
  outcome = interpret_synthesizer_outcome(outcome, plugin, path, reporter)
508
522
  next if outcome.nil? || outcome.empty?
509
523
 
@@ -514,23 +528,6 @@ module Rigor
514
528
  result
515
529
  end
516
530
 
517
- # ADR-32 WD5 — cache wrapper around a single (plugin, file) invocation. The cache stores the empty
518
- # string `""` as the "no contribution" sentinel because `Cache::Store` treats `nil` as a cache miss.
519
- # Error tuples are stored as the canonical `[:error, message_string]` Array so the same wrapper
520
- # short-circuits subsequent runs against unchanged broken input.
521
- def synthesizer_output_for(plugin, callable, path, cache_store)
522
- return invoke_synthesizer_safely(callable, path) if cache_store.nil?
523
- return invoke_synthesizer_safely(callable, path) unless File.file?(path)
524
-
525
- descriptor = build_synthesizer_cache_descriptor(plugin, path)
526
- cache_store.fetch_or_compute(
527
- producer_id: SYNTHESIZER_CACHE_PRODUCER_ID,
528
- params: {},
529
- descriptor: descriptor,
530
- generation_cap: synthesizer_generation_cap
531
- ) { invoke_synthesizer_safely(callable, path) || "" }
532
- end
533
-
534
531
  # ADR-32 WD6 / WD12 — route a synthesizer return value through the per-run reporter. The
535
532
  # synthesizer's contract (declared in
536
533
  # `plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb`) admits four return shapes:
@@ -565,29 +562,78 @@ module Rigor
565
562
  reporter&.record(plugin_id: plugin.manifest.id, path: path, message: message.to_s, kind: kind)
566
563
  end
567
564
 
568
- # ADR-32 WD13 — turn each {Environment::RbsLoader#inline_member_standdowns} record into a WD12
569
- # `:not_honoured` entry, so the run's `plugin.rbs-inline.source-rbs-annotation-not-honoured` row names
570
- # BOTH sources and which one won. WD12's rule is what makes this necessary rather than optional: an
571
- # annotation Rigor parses and does not honour is reported, never swallowed — and a silent per-member
572
- # strip is exactly the shape that rule forbids.
565
+ # ADR-112 WD5 / issue #1075 — turn each reported {MemberConsistency::Record} into a row on the
566
+ # synthesis stream. A contradiction, or a refinement outside its own declared type, is a `:contradiction`
567
+ # entry ({Analysis::CheckRules::RULE_CONTRADICTING_SIGNATURE}, an error), positioned at the `.rbs`
568
+ # member when there is one: that file is real and its line is exact, where the inline member's position
569
+ # lives in the synthesized RBS buffer, a document nobody has. An undecided pair keeps ADR-32 WD12's
570
+ # `:not_honoured` row at the annotated `.rb` (line 1, as every entry on that stream is): the inline
571
+ # signature was parsed and dropped, and WD12 forbids swallowing that. The silent outcomes — equal, or
572
+ # merged to the more precise side — record nothing.
573
573
  #
574
- # The row is positioned at the annotated `.rb` (line 1, as every entry on this stream is): the member's
575
- # own position lives in the synthesized RBS buffer, which describes a document nobody has, and the
576
- # ADR-54 environment cache drops positions anyway, so a line read off it would differ warm and cold.
577
- def record_inline_member_standdowns(loader, reporter, root)
574
+ # The records are derived with a {MemberConsistency::RbsProof}, so a contradiction is proven from the
575
+ # RBS hierarchy of core and stdlib classes, never from the analyzer process's loaded constants.
576
+ def record_member_consistency(loader, reporter, root)
578
577
  return if reporter.nil?
579
578
 
580
- loader.inline_member_standdowns.each do |class_name, method_name, kind, signature_path, virtual_name|
581
- plugin_id, path = split_virtual_source_name(virtual_name)
582
- next if path.nil?
583
-
584
- reporter.record(
585
- plugin_id: plugin_id, path: path, kind: :not_honoured,
586
- message: inline_member_standdown_message(class_name, method_name, kind, signature_path, root)
587
- )
579
+ proof = MemberConsistency::RbsProof.new(loader)
580
+ loader.member_consistency(proof: proof).each do |record|
581
+ case record.outcome
582
+ when :contradiction then record_member_contradiction(reporter, record, root)
583
+ when :refinement then record_refinement_contradiction(reporter, record, root)
584
+ when :undecided then record_member_undecided(reporter, record, root)
585
+ end
588
586
  end
589
587
  end
590
588
 
589
+ def record_member_contradiction(reporter, record, root)
590
+ plugin_id, path = split_virtual_source_name(record.virtual_name)
591
+ return if path.nil?
592
+
593
+ reporter.record(
594
+ plugin_id: plugin_id, path: path_relative_to(record.signature_path, root), line: record.signature_line,
595
+ kind: :contradiction,
596
+ message: "`#{member_label(record)}` is declared here and by an inline annotation in " \
597
+ "`#{path_relative_to(path, root)}`, and the two contradict: #{record.detail}. Rigor reads " \
598
+ "this `.rbs` declaration. Regenerate the signature, or fix the annotation, so that one " \
599
+ "of the two refines the other."
600
+ )
601
+ end
602
+
603
+ def record_refinement_contradiction(reporter, record, root)
604
+ plugin_id, path = split_virtual_source_name(record.virtual_name)
605
+ path = record.signature_path if path.nil?
606
+ return if path.nil?
607
+
608
+ reporter.record(
609
+ plugin_id: plugin_id.to_s, path: path_relative_to(path, root), line: record.signature_line,
610
+ kind: :contradiction,
611
+ message: "`#{member_label(record)}` refines its own signature to a type outside it: " \
612
+ "#{record.detail}. A `rigor:v1:` refinement may narrow the ordinary RBS contract, never " \
613
+ "contradict it, so one of the two is wrong."
614
+ )
615
+ end
616
+
617
+ def record_member_undecided(reporter, record, root)
618
+ plugin_id, path = split_virtual_source_name(record.virtual_name)
619
+ return if path.nil?
620
+
621
+ reporter.record(
622
+ plugin_id: plugin_id, path: path, kind: :not_honoured,
623
+ message: "`#{member_label(record)}` is also declared in " \
624
+ "`#{path_relative_to(record.signature_path, root)}`, and Rigor cannot tell which of the two " \
625
+ "is the more precise (#{record.detail}), so the `.rbs` declaration binds and the inline " \
626
+ "signature was dropped. Left to collide the two would fail the class's definition build, " \
627
+ "and every call on it — real methods and typos alike — would read `Dynamic[top]`. Make " \
628
+ "one of the two a refinement of the other, or remove one, to settle which binds."
629
+ )
630
+ end
631
+
632
+ def member_label(record)
633
+ separator = record.kind == :singleton ? "." : "#"
634
+ "#{record.class_name}#{separator}#{record.method_name}"
635
+ end
636
+
591
637
  # Splits a virtual buffer name back into the pair {.collect_virtual_rbs} composed it from
592
638
  # (`virtual:<plugin id>:<source path>`). Limited to three fields so a path carrying a colon survives.
593
639
  # Returns `[nil, nil]` for a name in any other shape — a synthesizer channel this method does not own.
@@ -598,59 +644,10 @@ module Rigor
598
644
  [plugin_id, path]
599
645
  end
600
646
 
601
- def inline_member_standdown_message(class_name, method_name, kind, signature_path, root)
602
- separator = kind == :singleton ? "." : "#"
603
- "`#{class_name}#{separator}#{method_name}` is also declared in " \
604
- "`#{path_relative_to(signature_path, root)}`, and an explicit `.rbs` declaration wins over an " \
605
- "inline annotation for the same member, so the inline signature was dropped. Left to collide the " \
606
- "two would fail the class's definition build, and every call on it — real methods and typos " \
607
- "alike — would read `Dynamic[top]`. Remove one of the two declarations to make the inline " \
608
- "annotation bind."
609
- end
610
-
611
647
  def path_relative_to(path, root)
612
648
  prefix = "#{root}#{File::SEPARATOR}"
613
649
  path.to_s.start_with?(prefix) ? path.to_s.delete_prefix(prefix) : path.to_s
614
650
  end
615
-
616
- SYNTHESIZER_CACHE_PRODUCER_ID = "plugin.source_rbs_synthesizer"
617
- private_constant :SYNTHESIZER_CACHE_PRODUCER_ID
618
-
619
- # One entry per (plugin, source file), all of them live for as long as the file is in the project — a
620
- # generation count says nothing about staleness here, so this producer declares itself out of
621
- # `Cache::Store#evict!`'s compaction pass and is bounded only by the size-based LRU pass. A method
622
- # rather than a constant: `Cache::Store` is not loaded yet when this class body runs.
623
- def synthesizer_generation_cap
624
- Cache::Store::UNBOUNDED_GENERATIONS
625
- end
626
-
627
- def build_synthesizer_cache_descriptor(plugin, path)
628
- Cache::Descriptor.new(
629
- files: [Cache::Descriptor::FileEntry.new(
630
- path: path.to_s,
631
- comparator: :digest,
632
- value: synthesizer_input_digest(path)
633
- )],
634
- plugins: [plugin.plugin_entry]
635
- )
636
- end
637
-
638
- def synthesizer_input_digest(path)
639
- Digest::SHA256.hexdigest(File.binread(path))
640
- rescue ::SystemCallError
641
- # Unreadable file → key on the path alone; the synthesizer's File.file?/File.read will see the same
642
- # failure and return nil.
643
- Digest::SHA256.hexdigest(path.to_s)
644
- end
645
-
646
- def invoke_synthesizer_safely(callable, path)
647
- callable.call(path.to_s)
648
- rescue StandardError
649
- # WD6 fail-soft — a synthesizer that raises does NOT crash analysis. Unlike the `[:error, msg]`
650
- # return path (which the runner surfaces as `source-rbs-synthesis-failed`), an unhandled raise is
651
- # swallowed silently; the unexamined-raise channel is deliberately silent per WD6.
652
- nil
653
- end
654
651
  end
655
652
 
656
653
  # Resolves a constant name to a Rigor::Type::Nominal (the *instance* type carrier). Consults the static
@@ -696,6 +693,17 @@ module Rigor
696
693
  end
697
694
  end
698
695
 
696
+ # Issue #1362 — the translated type of the RBS declaration of the global variable `name` (`$stdout: IO` answers
697
+ # `Nominal[IO]`), or nil when no signature declares it or the environment carries no RBS loader. With
698
+ # `builtin: true` it answers only for a declaration in Ruby's own core or stdlib signatures, a global the
699
+ # interpreter sets; a project's or a gem's declaration then answers nil.
700
+ def global_for_name(name, builtin: false)
701
+ return nil if rbs_loader.nil?
702
+ return nil if builtin && !rbs_loader.core_or_stdlib_global?(name)
703
+
704
+ rbs_loader.global_type(name)
705
+ end
706
+
699
707
  # Returns true when the constant name is known to either the static registry or the RBS loader. Useful
700
708
  # for callers that only need a presence check without materialising a type carrier.
701
709
  def class_known?(name)