rigortype 0.3.7 → 0.3.9

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 (450) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/gem_overlay/activesupport/core_ext.rbs +109 -13
  5. data/docs/handbook/02-everyday-types.md +17 -14
  6. data/docs/handbook/03-narrowing.md +37 -4
  7. data/docs/handbook/05-methods-and-blocks.md +1 -1
  8. data/docs/handbook/06-classes.md +2 -2
  9. data/docs/handbook/07-rbs-and-extended.md +14 -1
  10. data/docs/handbook/11-sig-gen.md +96 -15
  11. data/docs/handbook/README.md +1 -1
  12. data/docs/handbook/appendix-elixir.md +2 -2
  13. data/docs/handbook/appendix-go.md +2 -2
  14. data/docs/handbook/appendix-java-csharp.md +2 -2
  15. data/docs/handbook/appendix-mypy.md +2 -2
  16. data/docs/handbook/appendix-phpstan.md +1 -1
  17. data/docs/handbook/appendix-rust.md +1 -1
  18. data/docs/handbook/appendix-type-theory.md +4 -4
  19. data/docs/handbook/appendix-typescript.md +1 -1
  20. data/docs/llms.txt +2 -0
  21. data/docs/manual/02-cli-reference.md +39 -4
  22. data/docs/manual/03-configuration.md +11 -0
  23. data/docs/manual/04-diagnostics.md +12 -1
  24. data/docs/manual/07-plugins.md +15 -5
  25. data/docs/manual/08-skills.md +23 -2
  26. data/docs/manual/11-ci.md +9 -0
  27. data/docs/manual/15-type-protection-coverage.md +8 -0
  28. data/docs/manual/16-rbs-extended-annotations.md +6 -1
  29. data/docs/manual/18-removing-dead-code.md +10 -8
  30. data/docs/manual/plugins/rigor-actionpack.md +18 -0
  31. data/docs/manual/plugins/rigor-activerecord.md +28 -0
  32. data/docs/manual/plugins/rigor-activesupport-core-ext.md +32 -2
  33. data/docs/manual/plugins/rigor-rbs-inline.md +42 -1
  34. data/lib/rigor/analysis/baseline.rb +2 -2
  35. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  36. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  37. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  38. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +1 -1
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  40. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/rule_walk.rb +39 -4
  42. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  43. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  44. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  45. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  46. data/lib/rigor/analysis/check_rules.rb +200 -32
  47. data/lib/rigor/analysis/crash_signature.rb +69 -13
  48. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  52. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  53. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  54. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  55. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  56. data/lib/rigor/analysis/fact_store.rb +6 -1
  57. data/lib/rigor/analysis/incremental.rb +16 -0
  58. data/lib/rigor/analysis/incremental_session.rb +100 -29
  59. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  60. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  61. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -5
  62. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  63. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -2
  65. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  66. data/lib/rigor/analysis/result.rb +1 -3
  67. data/lib/rigor/analysis/rule_catalog.rb +4 -1
  68. data/lib/rigor/analysis/run_cache_key.rb +1 -1
  69. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  70. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  71. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  72. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +316 -40
  73. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  74. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -10
  75. data/lib/rigor/analysis/runner/pool_coordinator.rb +430 -77
  76. data/lib/rigor/analysis/runner/project_pre_passes.rb +12 -6
  77. data/lib/rigor/analysis/runner/run_snapshots.rb +13 -3
  78. data/lib/rigor/analysis/runner.rb +200 -43
  79. data/lib/rigor/analysis/worker_session.rb +36 -12
  80. data/lib/rigor/bleeding_edge.rb +12 -24
  81. data/lib/rigor/builtins/hkt_builtins.rb +2 -2
  82. data/lib/rigor/builtins/imported_refinements.rb +166 -30
  83. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  84. data/lib/rigor/builtins/regex_refinement.rb +4 -4
  85. data/lib/rigor/builtins/static_return_refinements.rb +5 -6
  86. data/lib/rigor/cache/annotation_location.rb +70 -0
  87. data/lib/rigor/cache/descriptor.rb +58 -20
  88. data/lib/rigor/cache/engine_source.rb +4 -4
  89. data/lib/rigor/cache/file_digest.rb +9 -1
  90. data/lib/rigor/cache/incremental_snapshot.rb +40 -6
  91. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  92. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  93. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  94. data/lib/rigor/cache/rbs_descriptor.rb +79 -11
  95. data/lib/rigor/cache/rbs_environment.rb +8 -4
  96. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +39 -0
  97. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  98. data/lib/rigor/cache/store.rb +85 -33
  99. data/lib/rigor/cli/annotate_command.rb +5 -6
  100. data/lib/rigor/cli/check_command.rb +57 -10
  101. data/lib/rigor/cli/check_invocation.rb +6 -11
  102. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  103. data/lib/rigor/cli/coverage_command.rb +1 -1
  104. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  105. data/lib/rigor/cli/coverage_scan.rb +1 -5
  106. data/lib/rigor/cli/diff_command.rb +1 -1
  107. data/lib/rigor/cli/doc_links.rb +3 -3
  108. data/lib/rigor/cli/docs_command.rb +2 -2
  109. data/lib/rigor/cli/doctor_command.rb +85 -38
  110. data/lib/rigor/cli/effects_command.rb +2 -2
  111. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  112. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  113. data/lib/rigor/cli/explain_command.rb +34 -1
  114. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  115. data/lib/rigor/cli/lsp_command.rb +1 -1
  116. data/lib/rigor/cli/mcp_command.rb +1 -1
  117. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  118. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  119. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  120. data/lib/rigor/cli/plugin_command.rb +1 -1
  121. data/lib/rigor/cli/plugins_command.rb +2 -1
  122. data/lib/rigor/cli/prism_colorizer.rb +2 -2
  123. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  124. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  125. data/lib/rigor/cli/sig_gen_command.rb +31 -2
  126. data/lib/rigor/cli/skill_command.rb +1 -1
  127. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  128. data/lib/rigor/cli/skill_describe.rb +36 -35
  129. data/lib/rigor/cli/trace_command.rb +4 -4
  130. data/lib/rigor/cli/trace_renderer.rb +5 -6
  131. data/lib/rigor/cli/triage_command.rb +1 -1
  132. data/lib/rigor/cli/type_of_command.rb +10 -6
  133. data/lib/rigor/cli/type_scan_command.rb +4 -4
  134. data/lib/rigor/cli/unused_command.rb +11 -3
  135. data/lib/rigor/cli/upgrade_command.rb +1 -1
  136. data/lib/rigor/cli.rb +68 -6
  137. data/lib/rigor/config_audit.rb +30 -5
  138. data/lib/rigor/configuration/severity_profile.rb +6 -6
  139. data/lib/rigor/configuration.rb +43 -4
  140. data/lib/rigor/effects/attribution.rb +1 -3
  141. data/lib/rigor/effects/config_envelopes.rb +6 -8
  142. data/lib/rigor/effects/definition_lines.rb +25 -6
  143. data/lib/rigor/effects/effect_table.rb +0 -1
  144. data/lib/rigor/effects/entry_points.rb +0 -2
  145. data/lib/rigor/effects/envelope_check.rb +8 -8
  146. data/lib/rigor/effects/envelope_index.rb +5 -8
  147. data/lib/rigor/effects/framework_units.rb +4 -6
  148. data/lib/rigor/effects/identity.rb +4 -6
  149. data/lib/rigor/effects/inline_anchor.rb +7 -7
  150. data/lib/rigor/effects/label_intent.rb +3 -4
  151. data/lib/rigor/effects/liskov_check.rb +8 -8
  152. data/lib/rigor/effects/method_key.rb +1 -1
  153. data/lib/rigor/effects/plugin_facts.rb +4 -6
  154. data/lib/rigor/effects/propagator.rb +2 -3
  155. data/lib/rigor/effects/scanner.rb +1 -1
  156. data/lib/rigor/effects/signature_sources.rb +3 -5
  157. data/lib/rigor/effects/snapshot.rb +10 -10
  158. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  159. data/lib/rigor/effects/unit_scan.rb +10 -10
  160. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  161. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  162. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  163. data/lib/rigor/environment/class_registry.rb +3 -1
  164. data/lib/rigor/environment/failure_slot.rb +28 -0
  165. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  166. data/lib/rigor/environment/lockfile_resolver.rb +51 -4
  167. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  168. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  169. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  170. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  171. data/lib/rigor/environment/rbs_loader.rb +613 -113
  172. data/lib/rigor/environment.rb +189 -27
  173. data/lib/rigor/flow_contribution/merger.rb +0 -2
  174. data/lib/rigor/flow_contribution.rb +11 -13
  175. data/lib/rigor/inference/acceptance.rb +153 -8
  176. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  177. data/lib/rigor/inference/body_fixpoint.rb +5 -5
  178. data/lib/rigor/inference/budget_trace.rb +1 -5
  179. data/lib/rigor/inference/captured_locals.rb +2 -3
  180. data/lib/rigor/inference/closure_escape_analyzer.rb +2 -4
  181. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  182. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  183. data/lib/rigor/inference/element_read_widening.rb +168 -0
  184. data/lib/rigor/inference/expression_typer.rb +763 -143
  185. data/lib/rigor/inference/fork_map.rb +5 -7
  186. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  187. data/lib/rigor/inference/hkt_registry.rb +19 -10
  188. data/lib/rigor/inference/hkt_sugar_translator.rb +15 -21
  189. data/lib/rigor/inference/index_write_widening.rb +1 -5
  190. data/lib/rigor/inference/macro_block_self_type.rb +1 -4
  191. data/lib/rigor/inference/method_dispatcher/block_folding.rb +2 -9
  192. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  193. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  194. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  195. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +2 -2
  197. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  199. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  200. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  201. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +61 -47
  202. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  203. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +165 -31
  204. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  205. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  206. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  208. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -1
  209. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  210. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  211. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -5
  212. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  213. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  214. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +12 -13
  217. data/lib/rigor/inference/method_parameter_binder.rb +4 -6
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -3
  219. data/lib/rigor/inference/mutation_rejoin.rb +168 -0
  220. data/lib/rigor/inference/mutation_widening.rb +100 -51
  221. data/lib/rigor/inference/narrowing.rb +299 -46
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -7
  223. data/lib/rigor/inference/origin_lookup.rb +2 -3
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  226. data/lib/rigor/inference/precision_scanner.rb +3 -4
  227. data/lib/rigor/inference/project_patched_methods.rb +2 -2
  228. data/lib/rigor/inference/project_patched_scanner.rb +3 -3
  229. data/lib/rigor/inference/protection_scanner.rb +1 -2
  230. data/lib/rigor/inference/range_constant.rb +57 -0
  231. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  232. data/lib/rigor/inference/receiver_alias.rb +3 -3
  233. data/lib/rigor/inference/refinement_mutation.rb +72 -0
  234. data/lib/rigor/inference/scope_indexer.rb +688 -151
  235. data/lib/rigor/inference/statement_evaluator.rb +221 -50
  236. data/lib/rigor/inference/string_mutation.rb +60 -0
  237. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  238. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  239. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  240. data/lib/rigor/inference/version_guard.rb +28 -21
  241. data/lib/rigor/inference/void_origin.rb +3 -3
  242. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  243. data/lib/rigor/language_server/buffer_table.rb +4 -4
  244. data/lib/rigor/language_server/completion_provider.rb +1 -1
  245. data/lib/rigor/language_server/debouncer.rb +1 -1
  246. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  247. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  248. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  249. data/lib/rigor/language_server/hover_provider.rb +1 -1
  250. data/lib/rigor/language_server/hover_renderer.rb +2 -2
  251. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  252. data/lib/rigor/language_server/project_context.rb +2 -5
  253. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  254. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  255. data/lib/rigor/language_server/server.rb +9 -9
  256. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  257. data/lib/rigor/language_server/uri.rb +1 -1
  258. data/lib/rigor/plugin/base.rb +90 -21
  259. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  260. data/lib/rigor/plugin/effect_attribution.rb +7 -7
  261. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  262. data/lib/rigor/plugin/fact_store.rb +6 -6
  263. data/lib/rigor/plugin/io_boundary.rb +93 -8
  264. data/lib/rigor/plugin/isolation.rb +29 -6
  265. data/lib/rigor/plugin/loader.rb +31 -12
  266. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  267. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  268. data/lib/rigor/plugin/manifest.rb +23 -1
  269. data/lib/rigor/plugin/registry.rb +67 -14
  270. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +12 -1
  271. data/lib/rigor/plugin/trust_policy.rb +2 -4
  272. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  273. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  274. data/lib/rigor/project_environment.rb +138 -0
  275. data/lib/rigor/protection/analysis_guard.rb +93 -14
  276. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  277. data/lib/rigor/protection/dependency_closure.rb +5 -8
  278. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  279. data/lib/rigor/protection/discovery_seed.rb +11 -11
  280. data/lib/rigor/protection/kill_signature.rb +2 -4
  281. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  282. data/lib/rigor/protection/mutation_cache.rb +6 -9
  283. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  284. data/lib/rigor/protection/mutator.rb +2 -1
  285. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  286. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  287. data/lib/rigor/rbs_extended/envelope_scanner.rb +12 -13
  288. data/lib/rigor/rbs_extended/hkt_directives.rb +16 -1
  289. data/lib/rigor/rbs_extended/reporter.rb +122 -16
  290. data/lib/rigor/rbs_extended.rb +67 -22
  291. data/lib/rigor/reflection/constant_path.rb +126 -0
  292. data/lib/rigor/reflection.rb +26 -10
  293. data/lib/rigor/runtime/jit.rb +6 -8
  294. data/lib/rigor/scope/discovery_index.rb +30 -0
  295. data/lib/rigor/scope.rb +203 -4
  296. data/lib/rigor/sig_gen/generator.rb +181 -102
  297. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  298. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  299. data/lib/rigor/sig_gen/observation_collector.rb +17 -15
  300. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  301. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  302. data/lib/rigor/sig_gen/renderer.rb +17 -11
  303. data/lib/rigor/sig_gen/skip_reason_catalog.rb +111 -0
  304. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  305. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  306. data/lib/rigor/sig_gen/writer.rb +23 -6
  307. data/lib/rigor/signature_path_audit.rb +153 -6
  308. data/lib/rigor/source/literals.rb +0 -23
  309. data/lib/rigor/source/node_children.rb +0 -2
  310. data/lib/rigor/source/node_locator.rb +5 -11
  311. data/lib/rigor/source/node_walker.rb +2 -5
  312. data/lib/rigor/triage/catalogue.rb +1 -3
  313. data/lib/rigor/triage.rb +3 -5
  314. data/lib/rigor/type/accepts_result.rb +24 -6
  315. data/lib/rigor/type/combinator.rb +68 -5
  316. data/lib/rigor/type/data_class.rb +2 -2
  317. data/lib/rigor/type/data_instance.rb +4 -4
  318. data/lib/rigor/type/float_range.rb +128 -0
  319. data/lib/rigor/type/hash_shape.rb +5 -5
  320. data/lib/rigor/type/integer_range.rb +13 -8
  321. data/lib/rigor/type/nominal.rb +8 -2
  322. data/lib/rigor/type/refined.rb +3 -3
  323. data/lib/rigor/type/struct_class.rb +3 -3
  324. data/lib/rigor/type/struct_instance.rb +4 -4
  325. data/lib/rigor/type.rb +1 -0
  326. data/lib/rigor/type_node/generic.rb +1 -1
  327. data/lib/rigor/type_node/range_literal.rb +25 -0
  328. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  329. data/lib/rigor/type_node.rb +1 -0
  330. data/lib/rigor/version.rb +1 -1
  331. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  332. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  333. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  334. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -0
  335. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  336. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  337. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  338. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +1 -0
  339. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +33 -30
  340. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  341. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  342. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +28 -2
  343. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  344. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  345. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  346. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  347. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  348. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  349. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +1 -0
  350. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +3 -4
  351. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  352. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  353. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +42 -7
  354. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  355. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -0
  356. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  357. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +119 -14
  358. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  359. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +1 -0
  360. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  361. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  362. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  363. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  364. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  365. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  366. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  367. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  368. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  369. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  370. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  371. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  372. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  373. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  374. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  375. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +6 -3
  376. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  377. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  378. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +0 -3
  379. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  380. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  381. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  382. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  383. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  384. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +1 -0
  385. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  386. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  387. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  388. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -0
  389. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  390. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -10
  391. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +3 -3
  392. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +2 -2
  393. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  394. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  395. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  396. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  397. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -0
  398. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  399. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  400. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +128 -3
  401. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  402. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  403. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  404. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  405. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  406. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  407. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  408. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  409. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  410. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  411. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  412. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  413. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -1
  414. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  416. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +1 -0
  417. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  421. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  422. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  423. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +4 -5
  424. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  425. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +5 -5
  426. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  428. data/sig/rigor/cache.rbs +2 -3
  429. data/sig/rigor/environment.rbs +11 -0
  430. data/sig/rigor/inference.rbs +3 -10
  431. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  432. data/sig/rigor/plugin/manifest.rbs +2 -1
  433. data/sig/rigor/reflection.rbs +3 -1
  434. data/sig/rigor/scope.rbs +24 -3
  435. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -0
  436. data/sig/rigor/type.rbs +27 -1
  437. data/sig/rigor.rbs +1 -0
  438. data/skills/rigor-ask/SKILL.md +8 -6
  439. data/skills/rigor-plugin-author/references/02-walker-and-types.md +13 -2
  440. data/skills/rigor-project-init/SKILL.md +6 -3
  441. data/skills/rigor-project-init/references/02-configure.md +10 -6
  442. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  443. data/skills/rigor-type-oracle/SKILL.md +228 -0
  444. data/skills/rigor-type-oracle/references/01-oracle-commands.md +261 -0
  445. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  446. data/skills/rigor-type-oracle/references/03-gap-protocol.md +128 -0
  447. data/skills/rigor-unused-adjudicate/SKILL.md +8 -5
  448. metadata +28 -3
  449. data/lib/rigor/cli/probe_environment.rb +0 -85
  450. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -84,11 +84,24 @@ module Rigor
84
84
  # sigs are Rigor-shipped and trusted, so they stay on the loader's fast batch path.
85
85
  add_bundled_signatures(rbs_loader, loaded_libraries.to_set(&:to_s))
86
86
  env = RBS::Environment.from_loader(rbs_loader)
87
+ project_files = project_sig_files(signature_paths)
87
88
  add_project_signatures(env, signature_paths, deferred_signature_paths)
88
- add_virtual_rbs(env, virtual_rbs)
89
+ # Issue #928 — the capability-role catalog goes in AFTER the project's own signatures, per
90
+ # declaration, so a project that already declares one of these interface names keeps its own
91
+ # (`add_capability_role_signatures`). The rest of the bundled RBS cannot be ordered that way (it
92
+ # is what `from_loader` builds the env FROM), which is why a colliding project file is quarantined
93
+ # instead — and losing a whole `sig/` file to a name as ordinary as `_Closable` is exactly what
94
+ # shipping a top-level interface catalog would otherwise cost.
95
+ add_capability_role_signatures(env)
96
+ # Issue #824 — the project's own signatures are already in `env` at this point, so the inline
97
+ # contribution can be asked the question that keeps the two from colliding: which of its members
98
+ # does a `.rbs` file already declare? Those stand down per member ({.add_virtual_rbs}).
99
+ add_virtual_rbs(env, virtual_rbs, sig_member_owners: signature_member_owners(project_files, virtual_rbs))
89
100
  synthesize_missing_namespaces(env)
90
- env, resolved = resolve_quarantining_virtual_collisions(env, virtual_rbs)
91
- stub_missing_referenced_types(env, resolved, project_sig_files(signature_paths))
101
+ # Issue #777 — resolve-time backstop: also unload colliding PROJECT buffers (class-vs-module /
102
+ # constant redeclarations against bundled RBS). Virtual culprits are preferred when both appear.
103
+ env, resolved = resolve_quarantining_virtual_collisions(env, virtual_rbs, project_files: project_files)
104
+ stub_missing_referenced_types(env, resolved, project_files)
92
105
  end
93
106
 
94
107
  # rbs ships `stdlib/bigdecimal/` and `stdlib/bigdecimal-math/` as two libraries, so
@@ -116,8 +129,8 @@ module Rigor
116
129
  BIG_MATH_SIG_BASENAME = "big_math.rbs"
117
130
  private_constant :BIG_MATH_SIG_BASENAME
118
131
 
119
- # @param libraries [Array<String>] the resolved library list, `DEFAULT_LIBRARIES` included.
120
- # @return [Array<String>] the same list, minus `bigdecimal-math` when `bigdecimal` already brings
132
+ # @param libraries — the resolved library list, `DEFAULT_LIBRARIES` included.
133
+ # @return the same list, minus `bigdecimal-math` when `bigdecimal` already brings
121
134
  # `BigMath` in.
122
135
  def libraries_without_shadowed_bigdecimal_math(libraries)
123
136
  return libraries unless libraries.include?(BIGDECIMAL_LIBRARY) && libraries.include?(BIGDECIMAL_MATH_LIBRARY)
@@ -157,32 +170,61 @@ module Rigor
157
170
  false
158
171
  end
159
172
 
160
- # Backstop for a virtual-vs-anything `RBS::DuplicatedDeclarationError` that only materialises at
161
- # `resolve_type_names` (which rebuilds the env from `sources`). {.add_virtual_rbs}'s transactional
162
- # rescue already handles the add-time case — empirically everything on rbs 4.x — but the rbs gemspec
163
- # range spans `>= 3.0, < 5.0` (ADR-79) and WHERE duplicate detection fires is an rbs-internal choice
164
- # this code must not depend on. Resolution rule is the same as the add-time path: the explicit
165
- # signature wins, the colliding VIRTUAL buffer is dropped whole (`RBS::Environment#unload`) and
166
- # resolution retries; every pass removes at least one virtual buffer, so the loop is bounded by the
167
- # virtual-entry count. A duplicate involving no virtual buffer (sig-vs-sig), or an env without
168
- # `#unload` (rbs 3.x), re-raises into the existing one-warning degrade path.
173
+ # Backstop for a `RBS::DuplicatedDeclarationError` that only materialises at `resolve_type_names`
174
+ # (which rebuilds the env from `sources`). {.add_virtual_rbs} / {.add_project_parsed_decls}
175
+ # transactionally handle the add-time case — empirically everything on rbs 4.x — but the rbs
176
+ # gemspec range spans `>= 3.0, < 5.0` (ADR-79) and WHERE duplicate detection fires is an
177
+ # rbs-internal choice this code must not depend on. Resolution preference: drop colliding VIRTUAL
178
+ # buffers first (explicit `.rbs` wins), else drop colliding PROJECT `signature_paths:` buffers
179
+ # (issue #777 — bundled RBS wins over a kind-colliding project file), then retry. Every pass
180
+ # removes at least one buffer, so the loop is bounded. A duplicate with neither virtual nor
181
+ # project culprits (bundled-vs-bundled), or an env without `#unload` (rbs 3.x), re-raises into
182
+ # the existing one-warning total-failure path.
169
183
  #
170
184
  # The dropped set is not returned: consumers recover it from the built env via
171
185
  # {#virtual_rbs_collision_quarantined}, which also works on a cache HIT where this build never ran.
172
- def resolve_quarantining_virtual_collisions(env, virtual_rbs)
186
+ def resolve_quarantining_virtual_collisions(env, virtual_rbs, project_files: [])
173
187
  virtual_names = virtual_rbs.to_set { |name, _content| name.to_s }
174
- (virtual_names.size + 1).times do
188
+ project_names = Array(project_files).to_set(&:to_s)
189
+ # Bound by virtual + project culprit candidates: each pass removes at least one buffer.
190
+ (virtual_names.size + project_names.size + 1).times do
175
191
  return [env, env.resolve_type_names]
176
192
  rescue ::RBS::DuplicatedDeclarationError => e
177
- raise unless env.respond_to?(:unload)
193
+ env = unload_duplicated_declaration_culprits(
194
+ env, e, virtual_names: virtual_names, project_names: project_names
195
+ )
196
+ end
197
+ [env, env.resolve_type_names]
198
+ end
199
+
200
+ # Prefer dropping colliding VIRTUAL buffers (explicit `.rbs` wins), else PROJECT `signature_paths:`
201
+ # buffers (issue #777 — bundled RBS wins over a kind-colliding project file). A duplicate with
202
+ # neither virtual nor project culprits (bundled-vs-bundled), or an env without `#unload`, re-raises.
203
+ def unload_duplicated_declaration_culprits(env, error, virtual_names:, project_names:)
204
+ raise unless env.respond_to?(:unload)
205
+
206
+ culprits = duplicated_declaration_buffer_names(error)
207
+ virtual_culprits = culprits.select { |name| virtual_names.include?(name.to_s) }
208
+ return env.unload(virtual_culprits) unless virtual_culprits.empty?
178
209
 
179
- culprits = e.decls.filter_map { |decl| decl.location&.buffer&.name }
180
- .uniq.select { |name| virtual_names.include?(name) }
181
- raise if culprits.empty?
210
+ # Prefer dropping the project buffer(s): the bundled declaration stays, Greeter/core stay
211
+ # usable, and only the conflicting file is absent.
212
+ project_culprits = select_project_collision_culprits(culprits, project_names)
213
+ raise if project_culprits.empty?
182
214
 
183
- env = env.unload(culprits)
215
+ env.unload(project_culprits)
216
+ end
217
+
218
+ def duplicated_declaration_buffer_names(error)
219
+ error.decls.filter_map { |decl| decl.location&.buffer&.name }.uniq
220
+ end
221
+
222
+ def select_project_collision_culprits(culprits, project_names)
223
+ culprits.select do |name|
224
+ project_names.include?(File.expand_path(name.to_s))
225
+ rescue ArgumentError, TypeError
226
+ project_names.include?(name.to_s)
184
227
  end
185
- [env, env.resolve_type_names]
186
228
  end
187
229
 
188
230
  # ADR-5 robustness, second tier. A project `signature_paths:` RBS that *references* a type no loaded
@@ -298,11 +340,26 @@ module Rigor
298
340
  next if parsed.nil? # quarantined (unparseable) or unreadable — skip so the env survives
299
341
 
300
342
  buffer, directives, decls = parsed
301
- add_parsed_decls(env, buffer, directives, decls)
343
+ add_project_parsed_decls(env, buffer, directives, decls)
302
344
  end
303
345
  add_deferred_signatures(env, deferred)
304
346
  end
305
347
 
348
+ # Issue #777 — add one project signature transactionally. `RBS::Environment#add_source` appends to
349
+ # `sources` BEFORE inserting decls, so a mid-insert `RBS::DuplicatedDeclarationError` (class-vs-module
350
+ # kind collision against bundled RBS, or a constant redeclared from core) would otherwise leave a
351
+ # poisoned source that `resolve_type_names` re-raises outside every per-file rescue, collapsing the
352
+ # WHOLE env to nil. Mirror {.add_virtual_rbs}: drop the poisoned source and continue so Greeter /
353
+ # core / every non-conflicting project file still load. Without a `sources` table (rbs 3.x) re-raise
354
+ # into the existing total-failure path — we cannot make the skip transactional there.
355
+ def add_project_parsed_decls(env, buffer, directives, decls)
356
+ add_parsed_decls(env, buffer, directives, decls)
357
+ rescue ::RBS::DuplicatedDeclarationError
358
+ raise unless env.respond_to?(:sources)
359
+
360
+ env.sources.reject! { |source| source.buffer.name == buffer.name }
361
+ end
362
+
306
363
  # Issue #610 — the DEFERRED half: signature files a bundled plugin contributes through its manifest,
307
364
  # added after every other source so each can be asked a question the eager half cannot be asked —
308
365
  # "does something already declare this class, at a different generic arity?".
@@ -331,12 +388,12 @@ module Rigor
331
388
  buffer, directives, decls = parsed
332
389
  next if generic_arity_conflict(env, decls)
333
390
 
334
- add_parsed_decls(env, buffer, directives, decls)
391
+ add_project_parsed_decls(env, buffer, directives, decls)
335
392
  end
336
393
  end
337
394
 
338
395
  # Issue #610 — which deferred (plugin-contributed) files stood down, as
339
- # `[absolute_path, class_name, existing_arity, incoming_arity]`, sorted. DERIVED from a built env
396
+ # `[absolute_path, class_name, existing_arity, incoming_arity, existing_file]`, sorted. DERIVED from a built env
340
397
  # rather than recorded during the build, for {RbsLoader#quarantined_signatures}' reason: a cache HIT
341
398
  # never runs the build, and a condition only the build knew would silently disappear on the second
342
399
  # run. Re-asking the same question of the FINAL env gives the same answer — the file's declaration is
@@ -353,21 +410,40 @@ module Rigor
353
410
  end
354
411
  end
355
412
 
356
- # The first `[class_name, existing_arity, incoming_arity]` where `decls` would re-declare a class the
357
- # env already has with a DIFFERENT number of type parameters, or nil when they agree everywhere.
358
- # Equal arity is not a conflict: RBS reopens the class, which is the ordinary and supported case, so
359
- # the stand-down must not fire on it.
413
+ # The first `[class_name, existing_arity, incoming_arity, existing_file]` where `decls` would
414
+ # re-declare a class the env already has with a DIFFERENT number of type parameters, or nil when
415
+ # they agree everywhere. Equal arity is not a conflict: RBS reopens the class, which is the ordinary
416
+ # and supported case, so the stand-down must not fire on it.
417
+ #
418
+ # The existing arity is read off the entry's FIRST declaration, never through `entry.type_params`:
419
+ # rbs 4.x's `ClassEntry#type_params` validates every declaration against the first before answering
420
+ # and raises `GenericParameterMismatchError` on an entry that already disagrees with itself (rbs
421
+ # 3.x's does not), so the derived reader ({.deferred_standdowns}) raised on exactly the env it exists
422
+ # to describe — one a user's own two sources collided, or one the cache served from before the
423
+ # producer passed the deferred list. The first declaration is also the one rbs compares against.
424
+ # `existing_file` is that declaration's buffer name — the source that displaced the plugin's — or
425
+ # nil when it has none or carries only the cached-position sentinel.
360
426
  def generic_arity_conflict(env, decls)
361
427
  each_declared_class(decls) do |name, arity|
362
428
  entry = env.class_decls[::RBS::TypeName.parse(name)]
363
- next if entry.nil? || !entry.respond_to?(:type_params)
429
+ next if entry.nil?
364
430
 
365
- existing = entry.type_params&.size
366
- return [name, existing, arity] if existing && existing != arity
431
+ first = entry_declarations(entry).first
432
+ existing = first.respond_to?(:type_params) ? first.type_params&.size : nil
433
+ return [name, existing, arity, declaration_buffer_name(first)] if existing && existing != arity
367
434
  end
368
435
  nil
369
436
  end
370
437
 
438
+ def declaration_buffer_name(decl)
439
+ location = decl.respond_to?(:location) ? decl.location : nil
440
+ buffer = location&.buffer
441
+ name = buffer.nil? ? nil : buffer.name.to_s
442
+ return nil if name.nil? || name.empty? || name == CACHED_LOCATION_BUFFER_NAME
443
+
444
+ name
445
+ end
446
+
371
447
  # Yields `[absolute_class_name, type_param_count]` for every class declared anywhere in `decls`,
372
448
  # descending through modules and nested classes. Names inside a `module`/`class` body are parsed
373
449
  # RELATIVE, so the enclosing path is accumulated here — the env is keyed by absolute name, and
@@ -436,6 +512,55 @@ module Rigor
436
512
  end
437
513
  end
438
514
 
515
+ # Issue #777 — project `signature_paths:` files that PARSE but are absent from the built env because
516
+ # {.add_project_parsed_decls} / {.resolve_quarantining_virtual_collisions} quarantined a duplicated
517
+ # declaration (typically class-vs-module or a constant already shipped by bundled RBS). Derived from
518
+ # the env like {#virtual_rbs_collision_quarantined}, so a cache HIT reports the same condition.
519
+ #
520
+ # Path membership alone is not enough: the suite's {RbsEnvMemo} reuses a byte-identical env across
521
+ # `Dir.mktmpdir` roots when `signature_paths:` are relative (`sig/sink.rbs`), leaving buffers named
522
+ # after the FIRST path. A later example's absolute path is then absent from `present` even though
523
+ # the declarations loaded — that used to spam `duplicated declaration against bundled RBS` banners
524
+ # (~60 per CI shard) for fixtures that never collided. Basename + content equality catches that
525
+ # reuse without hiding a real quarantine (e.g. project `class Base64` vs bundled `module Base64`,
526
+ # same basename, different bytes).
527
+ COLLISION_QUARANTINE_NOTE =
528
+ "duplicated declaration against bundled RBS — quarantined so the rest of the RBS env still loads"
529
+
530
+ def collision_quarantined_project_signatures(env, signature_paths)
531
+ return [] if env.nil?
532
+
533
+ present = env.buffers.to_set do |buffer|
534
+ name = buffer.name
535
+ next name.to_s unless name
536
+
537
+ File.expand_path(name.to_s)
538
+ rescue ArgumentError, TypeError
539
+ name.to_s
540
+ end
541
+ buffers_by_basename = env.buffers.group_by { |buffer| File.basename(buffer.name.to_s) }
542
+ project_sig_files(signature_paths).sort.filter_map do |file|
543
+ next if present.include?(file)
544
+ next if parse_signature_file(file).nil? # parse / encoding quarantine owns these
545
+ next if project_signature_loaded_under_other_path?(file, buffers_by_basename)
546
+
547
+ [file, "#{file}: #{COLLISION_QUARANTINE_NOTE}"]
548
+ end
549
+ end
550
+
551
+ # True when `file`'s bytes are already in `env` under a different absolute path (same basename).
552
+ # See {#collision_quarantined_project_signatures} — keeps memoised env reuse from looking like a
553
+ # collision quarantine.
554
+ def project_signature_loaded_under_other_path?(file, buffers_by_basename)
555
+ peers = buffers_by_basename[File.basename(file)]
556
+ return false if peers.nil? || peers.empty?
557
+
558
+ content = File.read(file, encoding: "UTF-8")
559
+ peers.any? { |buffer| buffer.content == content }
560
+ rescue Errno::ENOENT, Errno::EISDIR, Errno::EACCES
561
+ false
562
+ end
563
+
439
564
  # The `::`-stripped names of every type a PROJECT signature references that no loaded declaration
440
565
  # provides — the input to {.append_stub_declarations}.
441
566
  #
@@ -702,7 +827,12 @@ module Rigor
702
827
  # provenance (RBS parse errors cite it) — it is not a real file path. Per WD6 the synthesizer-emit
703
828
  # path is responsible for catching its own parse errors and returning `nil` rather than garbage; this
704
829
  # method assumes its input is parseable and only rescues `RBS::ParsingError` as a fail-soft.
705
- def add_virtual_rbs(env, virtual_rbs)
830
+ # @param sig_member_owners — issue #824 — `{[class, method, kind] => signature_path}`
831
+ # from {.signature_member_owners}. Every member it names is removed from the inline contribution
832
+ # before the declaration is added, so `sig/` wins per MEMBER instead of the two colliding and
833
+ # costing the whole class its method surface (ADR-32 WD13). The stand-down is reported, never
834
+ # silent: {RbsLoader#inline_member_standdowns} re-derives the same set for the run's `:info` row.
835
+ def add_virtual_rbs(env, virtual_rbs, sig_member_owners: {})
706
836
  return if virtual_rbs.nil? || virtual_rbs.empty?
707
837
 
708
838
  virtual_rbs.each do |filename, content|
@@ -713,6 +843,7 @@ module Rigor
713
843
 
714
844
  buffer = ::RBS::Buffer.new(name: filename.to_s, content: content.to_s)
715
845
  _, directives, decls = ::RBS::Parser.parse_signature(buffer)
846
+ strip_members_declared_in_signatures!(decls, sig_member_owners) unless sig_member_owners.empty?
716
847
  add_parsed_decls(env, buffer, directives, decls)
717
848
  rescue ::RBS::BaseError
718
849
  # WD6 fail-soft: a single broken virtual RBS contribution does not pull the whole env down — for
@@ -732,6 +863,187 @@ module Rigor
732
863
  end
733
864
  end
734
865
 
866
+ # Issue #824 / ADR-32 WD13 — the member-level half of inline-vs-`sig/` precedence.
867
+ #
868
+ # A method declared BOTH by a project `.rbs` and by an inline annotation in its own `.rb` lands twice
869
+ # in one `RBS::Environment`: rbs merges the two sources into a single `ClassEntry` and ranks neither,
870
+ # so `RBS::DefinitionBuilder::MethodBuilder::Methods#validate!` raises
871
+ # `RBS::DuplicatedMethodDefinitionError` and the WHOLE class loses its method surface — every call on
872
+ # it, real methods and typos alike, reads `Dynamic[top]`. Measured on Rigor's own tree while
873
+ # [#779](https://github.com/rigortype/rigor/pull/779) was evaluated: 17 of 234 annotated files
874
+ # overlapped `sig/`, and the collision took 44 classes down. Steep 2.1 / rbs 4.1 have no precedence
875
+ # rule to follow here (Steep reports the same collision as a signature diagnostic and the class still
876
+ # fails to build), so the spec's "SHOULD follow Steep 2.0" pointer resolves to nothing and the choice
877
+ # is Rigor's: the reviewed, `sig-gen`-managed `.rbs` wins, per member.
878
+ #
879
+ # @return `{[class_name, method_name, kind] => signature_path}` for every
880
+ # method-shaped member the project's own `signature_paths:` files declare and the inline sources
881
+ # also name. Empty — and free — for a project that contributes no inline RBS, which is every
882
+ # project without a `source_rbs_synthesizer:` plugin.
883
+ #
884
+ # The parse is scoped by a substring pre-filter over each file's text: a declaration has to spell its
885
+ # own name, so a signature file mentioning none of the names the inline sources declare cannot
886
+ # collide with them. The filter over-approximates (it matches a name in a comment or a type position
887
+ # too), so it can only cost an unnecessary parse, never miss a collision.
888
+ def signature_member_owners(project_files, virtual_rbs)
889
+ return {} if project_files.nil? || virtual_rbs.nil? || virtual_rbs.empty?
890
+
891
+ names = inline_declared_name_segments(virtual_rbs)
892
+ return {} if names.empty?
893
+
894
+ project_files.to_a.sort.each_with_object({}) do |file, owners|
895
+ decls = parse_signature_file_if_mentions(file, names)
896
+ next if decls.nil?
897
+
898
+ each_declared_member(decls) do |class_name, member|
899
+ member_method_keys(member).each do |method_name, kind|
900
+ owners[[class_name, method_name, kind]] ||= file
901
+ end
902
+ end
903
+ end
904
+ end
905
+
906
+ # The `[class, method, kind, signature_path, virtual_buffer_name]` record for every inline member
907
+ # {.add_virtual_rbs} stands down, sorted and deduplicated.
908
+ #
909
+ # Derived from the loader's own INPUTS — the synthesized sources and the project's signature files —
910
+ # never from the built environment, for {RbsLoader#quarantined_signatures}' reason turned inside out:
911
+ # a cache HIT never runs the build, and the stripped member leaves no trace in the environment to
912
+ # read back. The same two inputs answer identically warm and cold, which is what makes the reported
913
+ # set independent of cache state.
914
+ def inline_member_standdowns_for(signature_paths:, virtual_rbs:)
915
+ owners = signature_member_owners(project_sig_files(signature_paths), virtual_rbs)
916
+ return [].freeze if owners.empty?
917
+
918
+ records = virtual_rbs.flat_map do |name, content|
919
+ next [] if content.nil? || content.empty? || invalid_encoding?(content.to_s)
920
+
921
+ decls = parse_signature_source(name, content.to_s)
922
+ decls.nil? ? [] : member_standdowns_in(name, decls, owners)
923
+ end
924
+ records.uniq.sort_by { |record| [record[0], record[1].to_s, record[2].to_s] }.freeze
925
+ end
926
+
927
+ def member_standdowns_in(virtual_name, decls, owners)
928
+ records = []
929
+ each_declared_member(decls) do |class_name, member|
930
+ member_method_keys(member).each do |method_name, kind|
931
+ signature_path = owners[[class_name, method_name, kind]]
932
+ next if signature_path.nil?
933
+
934
+ records << [class_name, method_name, kind, signature_path, virtual_name.to_s].freeze
935
+ end
936
+ end
937
+ records
938
+ end
939
+
940
+ # Removes, in place, every member `owners` says a project signature already declares. Nested
941
+ # declarations are descended into after the rejection, and a declaration member contributes no method
942
+ # key, so nesting is never itself removed.
943
+ def strip_members_declared_in_signatures!(decls, owners, prefix = [])
944
+ Array(decls).each do |decl|
945
+ next unless declaration_with_members?(decl)
946
+
947
+ inner = prefix + [decl.name.to_s.delete_prefix("::")]
948
+ class_name = inner.join("::")
949
+ decl.members.reject! do |member|
950
+ member_method_keys(member).any? { |method_name, kind| owners.key?([class_name, method_name, kind]) }
951
+ end
952
+ strip_members_declared_in_signatures!(decl.members, owners, inner)
953
+ end
954
+ end
955
+
956
+ # Yields `[class_name, member]` for every member of every class / module declaration in `decls`,
957
+ # descending through nesting. The enclosing path is accumulated and the name rendered `::`-stripped,
958
+ # so a `module Foo; class Bar` and a `class Foo::Bar` elsewhere key the same — which is what makes a
959
+ # `sig/` declaration and an inline one comparable at all.
960
+ def each_declared_member(decls, prefix = [], &block)
961
+ Array(decls).each do |decl|
962
+ next unless declaration_with_members?(decl)
963
+
964
+ inner = prefix + [decl.name.to_s.delete_prefix("::")]
965
+ class_name = inner.join("::")
966
+ decl.members.each { |member| block.call(class_name, member) }
967
+ each_declared_member(decl.members, inner, &block)
968
+ end
969
+ end
970
+
971
+ def declaration_with_members?(decl)
972
+ (decl.is_a?(::RBS::AST::Declarations::Module) || decl.is_a?(::RBS::AST::Declarations::Class)) &&
973
+ decl.respond_to?(:members)
974
+ end
975
+
976
+ # The `[method_name, kind]` pairs one RBS member contributes to a definition build, mirroring
977
+ # `RBS::DefinitionBuilder::MethodBuilder#build_instance` / `#build_singleton` — which is what decides
978
+ # whether two members collide. An attribute contributes its reader and / or writer name (`foo`,
979
+ # `foo=`), an alias its new name, and `def self?.x` both sides. An `overloading?` member (`def x: ...
980
+ # | ...`) contributes NOTHING: rbs files those under `overloads` rather than `originals`, so they are
981
+ # designed to compose with an existing declaration and must not stand down against it.
982
+ def member_method_keys(member)
983
+ case member
984
+ when ::RBS::AST::Members::MethodDefinition then method_definition_keys(member)
985
+ when ::RBS::AST::Members::AttrReader then [[member.name, member.kind]]
986
+ when ::RBS::AST::Members::AttrWriter then [[:"#{member.name}=", member.kind]]
987
+ when ::RBS::AST::Members::AttrAccessor then [[member.name, member.kind], [:"#{member.name}=", member.kind]]
988
+ when ::RBS::AST::Members::Alias then [[member.new_name, member.kind]]
989
+ else []
990
+ end
991
+ end
992
+
993
+ def method_definition_keys(member)
994
+ return [] if member.respond_to?(:overloading?) && member.overloading?
995
+
996
+ case member.kind
997
+ when :instance then [[member.name, :instance]]
998
+ when :singleton then [[member.name, :singleton]]
999
+ else [[member.name, :instance], [member.name, :singleton]] # `def self?.x` defines both sides
1000
+ end
1001
+ end
1002
+
1003
+ # A class / module declaration header, as the pre-filter in {.signature_member_owners} reads it out of
1004
+ # synthesized RBS text.
1005
+ INLINE_DECLARATION_NAME = /^[ \t]*(?:class|module)[ \t]+([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)/
1006
+ private_constant :INLINE_DECLARATION_NAME
1007
+
1008
+ # Every `::`-separated segment of every class / module name the inline sources declare, read off the
1009
+ # synthesized TEXT rather than a parse: the set only has to be a SUPERSET (it feeds the pre-filter),
1010
+ # and a regexp over the handful of annotated files is far cheaper than parsing them a second time.
1011
+ # Entries {.add_virtual_rbs} would skip are skipped here too — an empty or invalid-UTF-8 contribution
1012
+ # declares nothing, and `String#scan` raises `ArgumentError` on invalid bytes rather than returning
1013
+ # no match.
1014
+ def inline_declared_name_segments(virtual_rbs)
1015
+ virtual_rbs.each_with_object(Set.new) do |(_name, content), acc|
1016
+ content = content.to_s
1017
+ next if content.empty? || invalid_encoding?(content)
1018
+
1019
+ content.scan(INLINE_DECLARATION_NAME) { |(declared)| acc.merge(declared.split("::")) }
1020
+ end
1021
+ end
1022
+
1023
+ # Parses one project signature file, or nil when it is unreadable, not valid UTF-8, unparseable, or
1024
+ # mentions none of `names` (so nothing in it can collide with the inline sources).
1025
+ def parse_signature_file_if_mentions(file, names)
1026
+ content = File.read(file, encoding: "UTF-8")
1027
+ return nil if invalid_encoding?(content)
1028
+ return nil if names.none? { |name| content.include?(name) }
1029
+
1030
+ parse_signature_source(file, content)
1031
+ rescue Errno::ENOENT, Errno::EISDIR, Errno::EACCES
1032
+ nil
1033
+ end
1034
+
1035
+ # The declarations of already-read RBS text, or nil when it does not parse. The twin of
1036
+ # {.parse_signature_file} for content the caller already holds (a synthesized virtual source, or a
1037
+ # signature file the pre-filter has just read).
1038
+ def parse_signature_source(name, content)
1039
+ _buffer, _directives, decls = ::RBS::Parser.parse_signature(
1040
+ ::RBS::Buffer.new(name: name.to_s, content: content)
1041
+ )
1042
+ decls
1043
+ rescue ::RBS::BaseError
1044
+ nil
1045
+ end
1046
+
735
1047
  # Per-gem `data/vendored_gem_sigs/<gem>/` directories that ship with Rigor. Each subdirectory is one
736
1048
  # gem's RBS surface (the `<gem>.rbs` file is the typical content; `LICENSE.upstream` records
737
1049
  # provenance). Coverage is deliberately scoped to the native-extension and "everywhere in Rails" gems
@@ -758,6 +1070,51 @@ module Rigor
758
1070
  [Pathname(CORE_OVERLAY_SIGS_ROOT)]
759
1071
  end
760
1072
 
1073
+ # Rigor's capability-role catalog (`data/capability_roles/`) — the structural interfaces ADR-1 and
1074
+ # `docs/type-specification/structural-interfaces-and-object-shapes.md` name as shipped, so a
1075
+ # `%a{rigor:v1:conforms-to _ClosableStream}` written by following the specification resolves in a
1076
+ # default run instead of failing soft to an unresolved-interface row. Public for the same reason
1077
+ # {.core_overlay_sig_paths} is: the cache descriptor digests these files into the env-blob key.
1078
+ CAPABILITY_ROLES_SIGS_ROOT = File.expand_path(
1079
+ "../../../data/capability_roles",
1080
+ __dir__
1081
+ ).freeze
1082
+
1083
+ def capability_role_sig_paths
1084
+ return [] unless File.directory?(CAPABILITY_ROLES_SIGS_ROOT)
1085
+
1086
+ [Pathname(CAPABILITY_ROLES_SIGS_ROOT)]
1087
+ end
1088
+
1089
+ # Adds the catalog to an env that already holds the project's own signatures, one INTERFACE at a
1090
+ # time, skipping any name the env already declares. Granularity is the declaration rather than the
1091
+ # file (the #610 stand-down's unit) because this one file carries five unrelated roles: a project
1092
+ # declaring `_Closable` must not also lose `_ClosableStream`.
1093
+ def add_capability_role_signatures(env)
1094
+ declared = declared_interface_names(env)
1095
+ capability_role_sig_paths.each do |dir|
1096
+ dir.children.sort.each do |file|
1097
+ next unless file.file? && file.extname == ".rbs"
1098
+
1099
+ parsed = parse_signature_file(file.to_s)
1100
+ next if parsed.nil?
1101
+
1102
+ buffer, directives, decls = parsed
1103
+ standing = decls.reject { |decl| declared.include?(decl.name.absolute!) }
1104
+ next if standing.empty?
1105
+
1106
+ add_parsed_decls(env, buffer, directives, standing)
1107
+ end
1108
+ end
1109
+ end
1110
+
1111
+ # @param env — the environment the project's signatures are already in.
1112
+ def declared_interface_names(env)
1113
+ return Set.new unless env.respond_to?(:interface_decls)
1114
+
1115
+ env.interface_decls.keys.to_set
1116
+ end
1117
+
761
1118
  # Bundled signature sources that SUPPLEMENT a stdlib library's own declarations rather than stand
762
1119
  # alone: each re-opens a class the named `RBS::EnvironmentLoader` library declares with a mixin
763
1120
  # (`CGI::QueryExtension`), a superclass (`Prism::Result`), or an `| ...` overload continuation
@@ -794,7 +1151,7 @@ module Rigor
794
1151
  # that Rigor's arithmetic-chain widening produces). The overlay is added per-file, not
795
1152
  # per-directory, because the `LIBRARY_SUPPLEMENT_CORE_OVERLAYS` files must be gated individually.
796
1153
  #
797
- # @param loaded_library_names [Set<String>] libraries that actually resolved on this loader.
1154
+ # @param loaded_library_names — libraries that actually resolved on this loader.
798
1155
  def add_bundled_signatures(rbs_loader, loaded_library_names)
799
1156
  vendored_gem_sig_paths.each do |path|
800
1157
  next unless path.directory?
@@ -814,10 +1171,10 @@ module Rigor
814
1171
  end
815
1172
  end
816
1173
 
817
- # @param supplements [Hash{String => String}] basename → gating library map.
818
- # @param path [Pathname] the vendored directory or overlay file to test.
819
- # @param loaded_library_names [Set<String>] libraries that actually resolved on this loader.
820
- # @return [Boolean] true when `path` carries no library dependency, or its library loaded.
1174
+ # @param supplements — basename → gating library map.
1175
+ # @param path — the vendored directory or overlay file to test.
1176
+ # @param loaded_library_names — libraries that actually resolved on this loader.
1177
+ # @return true when `path` carries no library dependency, or its library loaded.
821
1178
  def supplement_dependency_loaded?(supplements, path, loaded_library_names)
822
1179
  library = supplements[path.basename.to_s]
823
1180
  library.nil? || loaded_library_names.include?(library)
@@ -833,9 +1190,9 @@ module Rigor
833
1190
  __dir__
834
1191
  ).freeze
835
1192
 
836
- # @param gem_names [Enumerable<String>] overlay-eligible Gemfile.lock gem names (the caller filters
1193
+ # @param gem_names — overlay-eligible Gemfile.lock gem names (the caller filters
837
1194
  # to the `:missing`-coverage, no-conflicting-plugin set).
838
- # @return [Array<Pathname>] the bundled overlay directory for each gem that ships one; empty when
1195
+ # @return the bundled overlay directory for each gem that ships one; empty when
839
1196
  # none match or the overlay root is absent.
840
1197
  def gem_overlay_sig_paths(gem_names)
841
1198
  return [] unless File.directory?(GEM_OVERLAY_SIGS_ROOT)
@@ -846,9 +1203,9 @@ module Rigor
846
1203
  end
847
1204
  end
848
1205
 
849
- # @param path [String, Pathname] typically an entry from an {RbsLoader} instance's
1206
+ # @param path — typically an entry from an {RbsLoader} instance's
850
1207
  # {#signature_paths}.
851
- # @return [Boolean] whether `path` sits under the bundled gem-overlay root — what
1208
+ # @return whether `path` sits under the bundled gem-overlay root — what
852
1209
  # `CheckRules::GEM_OVERLAY_OPEN_RECEIVERS`'s gate consults so that a class name's membership in
853
1210
  # that list alone never grants the open-receiver exemption; the overlay directory that made the
854
1211
  # entry true must have actually loaded THIS run too (issue #632, tracked further by #660).
@@ -886,7 +1243,7 @@ module Rigor
886
1243
  # answer to the very same question, on the class that already owns the overlay's layout
887
1244
  # ({GEM_OVERLAY_SIGS_ROOT}, {.gem_overlay_sig_paths}).
888
1245
  #
889
- # @param signature_paths [Array<String, Pathname>] typically an {RbsLoader} instance's
1246
+ # @param signature_paths — typically an {RbsLoader} instance's
890
1247
  # {#signature_paths}. Takes the whole list rather than one entry so the twin's file set resolves
891
1248
  # once per question — `CheckRules` asks it per `ActiveSupport::Duration` receiver.
892
1249
  def gem_overlay_twin_signatures_loaded?(signature_paths)
@@ -901,7 +1258,7 @@ module Rigor
901
1258
  # id mapping and passes the id; this resolves the id to the engine's own bundled `sig/` and answers
902
1259
  # the filesystem question.
903
1260
  #
904
- # @param plugin_id [String] a manifest id, e.g. `"activesupport-core-ext"`.
1261
+ # @param plugin_id — a manifest id, e.g. `"activesupport-core-ext"`.
905
1262
  def bundled_overlay_twin_signatures_loaded?(plugin_id, signature_paths)
906
1263
  return false if signature_paths.nil? || signature_paths.empty?
907
1264
 
@@ -969,23 +1326,27 @@ module Rigor
969
1326
  end
970
1327
  end
971
1328
 
972
- attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs
1329
+ attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs, :deferred_signature_paths
973
1330
 
974
- # @param libraries [Array<String, Symbol>] stdlib library names to load on top of core (e.g.,
1331
+ # @param libraries — stdlib library names to load on top of core (e.g.,
975
1332
  # `["pathname", "json"]`). Empty by default. Each entry MUST correspond to a directory under the
976
1333
  # `rbs` gem's `stdlib/` tree; unknown names are silently dropped on environment build (the underlying
977
1334
  # `RBS::EnvironmentLoader` raises and we fail-soft).
978
- # @param signature_paths [Array<String, Pathname>] additional directories of `.rbs` files to load
1335
+ # @param signature_paths — additional directories of `.rbs` files to load
979
1336
  # (typically the project's `sig/` tree). Non-existent or non-directory paths are filtered out at
980
1337
  # build time so the loader stays robust to fixtures and bare repositories.
981
- # @param cache_store [Rigor::Cache::Store, nil] the persistent cache the loader threads through to
1338
+ # @param cache_store — the persistent cache the loader threads through to
982
1339
  # `RbsEnvironment`, `RbsKnownClassNames`, `RbsConstantTable`, `RbsClassAncestorTable`, and
983
1340
  # `RbsClassTypeParamNames` producers. Pass `nil` (the default) to skip caching; the runner threads
984
1341
  # its own Store through here when enabled.
985
- # @param virtual_rbs [Array<[String, String]>] ADR-32 WD4 — `[virtual_filename, rbs_source]` pairs
1342
+ # @param virtual_rbs — >] ADR-32 WD4 — `[virtual_filename, rbs_source]` pairs
986
1343
  # synthesised from project source by a plugin's `Manifest#source_rbs_synthesizer`. Merged into the
987
1344
  # env after `signature_paths:` and the vendored stubs. Pass `[]` (the default) when no
988
1345
  # synthesizer-emitting plugin is loaded.
1346
+ # @param deferred_signature_paths — the subset of `signature_paths:` a bundled plugin
1347
+ # contributed (issue #610): loaded last and allowed to stand down against a colliding generic
1348
+ # arity. Read back by the env-cache producer, which rebuilds the env from the loader's readers on
1349
+ # a miss — a list it cannot read is a stand-down that runs only on a `--no-cache` build.
989
1350
  def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [],
990
1351
  deferred_signature_paths: [])
991
1352
  @libraries = libraries.map(&:to_s).freeze
@@ -1000,8 +1361,10 @@ module Rigor
1000
1361
  # `.freeze`d (per ADR-15 reflection-facade contract) without losing the lazy-memo behaviour. Slot
1001
1362
  # names currently consulted: `:env`, `:env_loaded`, `:env_build_warned`, `:definition_build_warned`,
1002
1363
  # `:definition_build_details`, `:definition_build_reported`, `:definition_build_failures`,
1003
- # `:internal_demand`, `:builder`, `:reflection`, `:instance_definitions_table`,
1004
- # `:singleton_definitions_table`.
1364
+ # `:definition_build_deferred_count`, `:definition_build_deferred_first`,
1365
+ # `:definition_build_summary_warned`, `:inline_member_standdowns`, `:internal_demand`,
1366
+ # `:internal_demand_status`, `:builder`, `:reflection`,
1367
+ # `:instance_definitions_table`, `:singleton_definitions_table`.
1005
1368
  # Constructed via `Hash.new` (NOT a `{ ... }` literal) so Rigor's `HashShape` narrowing doesn't
1006
1369
  # infer a fixed key set from the initial state and fold post-initial slot reads (e.g.
1007
1370
  # `@state[:env_loaded]`) to a constant `nil`.
@@ -1029,15 +1392,32 @@ module Rigor
1029
1392
  # the stderr banner) reads it, and a cache HIT reaches it too — the env was built with the file already
1030
1393
  # quarantined, so the condition is invisible in the cached env itself.
1031
1394
  #
1032
- # @return [Array<Array(String, String)>] empty when every `signature_paths:` file parses.
1395
+ # @return empty when every `signature_paths:` file parses.
1033
1396
  def quarantined_signatures
1034
- @state[:quarantined] ||= self.class.quarantined_project_signatures(@signature_paths).freeze
1397
+ @state[:quarantined] ||= begin
1398
+ parse_quarantined = self.class.quarantined_project_signatures(@signature_paths)
1399
+ collision_quarantined = self.class.collision_quarantined_project_signatures(
1400
+ @state[:env], @signature_paths
1401
+ )
1402
+ parse_paths = parse_quarantined.to_set { |path, _note| path }
1403
+ # Issue #610 — a deferred file that STOOD DOWN is absent from the env by design, which is exactly
1404
+ # what the collision derivation reads as "quarantined". Derived from the same env, so a cache HIT
1405
+ # excludes it too; {#signature_standdowns} is where it is reported instead.
1406
+ stood_down = self.class.deferred_standdowns(@state[:env], @deferred_signature_paths).to_set(&:first)
1407
+ (
1408
+ parse_quarantined +
1409
+ collision_quarantined.reject do |entry|
1410
+ parse_paths.include?(entry[0]) || stood_down.include?(entry[0])
1411
+ end
1412
+ ).freeze
1413
+ end
1035
1414
  end
1036
1415
 
1037
1416
  # Issue #610 — plugin-contributed signature files that stood down against a colliding generic arity,
1038
- # as `[absolute_path, class_name, existing_arity, incoming_arity]`. Empty whenever no plugin sig
1039
- # collides, which is every project that has not run `rbs collection install` for a gem a bundled
1040
- # plugin also declares.
1417
+ # as `[absolute_path, class_name, existing_arity, incoming_arity, existing_file]`. Empty whenever no
1418
+ # plugin sig collides, which is every project that has not run `rbs collection install` for a gem a
1419
+ # bundled plugin also declares. Reported as `rbs.coverage.plugin-signature-stood-down` (`:info`); the
1420
+ # same file MUST NOT also appear in {#quarantined_signatures}.
1041
1421
  def signature_standdowns
1042
1422
  @state[:signature_standdowns] ||= self.class.deferred_standdowns(env, @deferred_signature_paths).freeze
1043
1423
  end
@@ -1083,14 +1463,15 @@ module Rigor
1083
1463
  end
1084
1464
 
1085
1465
  # The total RBS-environment build failure captured this run, or nil when the env built. Unlike
1086
- # {#quarantined_signatures} — which the env survives, one file lighter, and which is re-derived by
1087
- # re-parsing so a cache HIT reports it too — a total failure (typically `RBS::DuplicatedDeclarationError`:
1088
- # a `signature_paths:` entry redeclaring a constant/class Rigor's bundled RBS already ships) collapses the
1089
- # WHOLE env to nil. A failed build produces no cached success to hide behind (nothing is persisted, so
1090
- # every run re-attempts and re-raises), so this is captured directly in {#env}'s rescue rather than
1091
- # re-derived. Forcing `env` (any query does) populates it.
1466
+ # {#quarantined_signatures} — which the env survives, one file lighter, and which is re-derived so a
1467
+ # cache HIT reports it too — a total failure collapses the WHOLE env to nil. Project-vs-bundled
1468
+ # `RBS::DuplicatedDeclarationError`s are quarantined per-file since #777; this slot remains for
1469
+ # unrecoverable build errors (e.g. bundled-vs-bundled collisions, or hosts without a transactional
1470
+ # `sources`/`unload` API). A failed build produces no cached success to hide behind (nothing is
1471
+ # persisted, so every run re-attempts and re-raises), so this is captured directly in {#env}'s rescue
1472
+ # rather than re-derived. Forcing `env` (any query does) populates it.
1092
1473
  #
1093
- # @return [Array(String, String, Array<String>), nil] `[error_class_name, first_error_line,
1474
+ # @return `[error_class_name, first_error_line,
1094
1475
  # conflicting_buffer_names]`, or nil when the environment built successfully.
1095
1476
  def env_build_failure
1096
1477
  env unless @state[:env_loaded]
@@ -1114,7 +1495,7 @@ module Rigor
1114
1495
  # definition-build failure leaves no trace in the env at all — the env is fine; it is the BUILD over it
1115
1496
  # that raised — so the rescue is the only place that ever knows.
1116
1497
  #
1117
- # @return [Array<Array(String, String, String, Array<String>)>] `[class_name, error_class_name,
1498
+ # @return `[class_name, error_class_name,
1118
1499
  # first_error_line, conflicting_buffer_names]`, one per class, in first-failure order. Empty for a
1119
1500
  # healthy sig set, which is the common case.
1120
1501
  def definition_build_failures
@@ -1128,7 +1509,7 @@ module Rigor
1128
1509
  # the {#quarantined_signatures} trick — so a cache HIT, which never runs the build, reports the same
1129
1510
  # condition: the marshalled env simply lacks the dropped buffers.
1130
1511
  #
1131
- # @return [Array<String>] virtual buffer names (source-file paths) whose contribution was dropped.
1512
+ # @return virtual buffer names (source-file paths) whose contribution was dropped.
1132
1513
  def virtual_rbs_collision_quarantined
1133
1514
  @state[:virtual_rbs_collisions] ||= begin
1134
1515
  built = @state[:env]
@@ -1143,6 +1524,25 @@ module Rigor
1143
1524
  end
1144
1525
  end
1145
1526
 
1527
+ # Issue #824 / ADR-32 WD13 — the inline (rbs-inline) members that stood down because the project's own
1528
+ # `signature_paths:` already declare the same `(class, method, kind)`. The `.rbs` wins per member; the
1529
+ # rest of the file's annotations still bind, and the class keeps its method surface instead of failing
1530
+ # its definition build.
1531
+ #
1532
+ # The sibling of {#virtual_rbs_collision_quarantined} one granularity finer: that one names a whole
1533
+ # inline FILE dropped for a class-level duplicate, this one names a single member. Both are derived
1534
+ # rather than recorded, so a cache HIT — where no build ran — reports the same condition; here the
1535
+ # derivation reads the loader's inputs rather than the built env, because a stripped member leaves no
1536
+ # trace in the env at all.
1537
+ #
1538
+ # @return `[class_name, method_name, kind, signature_path,
1539
+ # virtual_buffer_name]` records, sorted. Empty (and free) whenever the project contributes no inline
1540
+ # RBS or none of it overlaps a signature file.
1541
+ def inline_member_standdowns
1542
+ @state[:inline_member_standdowns] ||=
1543
+ self.class.inline_member_standdowns_for(signature_paths: @signature_paths, virtual_rbs: @virtual_rbs)
1544
+ end
1545
+
1146
1546
  # The referenced-but-undeclared types {.stub_missing_referenced_types} stubbed so the project classes
1147
1547
  # that mention them could build (e.g. an unavailable `DRb::DRbServer`, or a stale
1148
1548
  # `Textbringer::EditorError`). Recovered off the built env like {#synthesized_namespaces}, so it
@@ -1306,7 +1706,7 @@ module Rigor
1306
1706
  {}.freeze
1307
1707
  end
1308
1708
 
1309
- # @return [RBS::Definition, nil] the resolved instance definition for `class_name`, or nil when the
1709
+ # @return the resolved instance definition for `class_name`, or nil when the
1310
1710
  # class is unknown or its definition cannot be built (RBS may raise on broken hierarchies; we
1311
1711
  # fail-soft and return nil so the caller can fall back).
1312
1712
  #
@@ -1332,7 +1732,6 @@ module Rigor
1332
1732
  definition
1333
1733
  end
1334
1734
 
1335
- # @return [RBS::Definition::Method, nil]
1336
1735
  def instance_method(class_name:, method_name:)
1337
1736
  definition = instance_definition(class_name)
1338
1737
  return nil unless definition
@@ -1340,7 +1739,7 @@ module Rigor
1340
1739
  definition.methods[method_name.to_sym]
1341
1740
  end
1342
1741
 
1343
- # @return [Array<Symbol>, nil] every instance-method name on `class_name` — own, inherited, and
1742
+ # @return every instance-method name on `class_name` — own, inherited, and
1344
1743
  # included — as resolved by `RBS::DefinitionBuilder`. Returns `nil` (NOT `[]`) when the class
1345
1744
  # definition cannot be built so callers can tell "no methods" apart from "unknown class". Used by the
1346
1745
  # `rigor:v1:conforms-to` presence check ({Rigor::RbsExtended::ConformanceChecker}).
@@ -1351,7 +1750,7 @@ module Rigor
1351
1750
  definition.methods.keys
1352
1751
  end
1353
1752
 
1354
- # @return [RBS::Definition, nil] the built definition for the RBS interface `interface_name`
1753
+ # @return the built definition for the RBS interface `interface_name`
1355
1754
  # (`_RewindableStream`), whose `.methods` are the required members (including interface-ancestor
1356
1755
  # members). Returns `nil` when the name does not resolve to a loaded interface (a typo, or the
1357
1756
  # defining library / sig set is not on the load path). Fail-soft on RBS build errors.
@@ -1366,15 +1765,15 @@ module Rigor
1366
1765
  nil
1367
1766
  end
1368
1767
 
1369
- # @return [Array<Symbol>, nil] every method name required by the RBS interface `interface_name`, or nil
1768
+ # @return every method name required by the RBS interface `interface_name`, or nil
1370
1769
  # when it does not resolve. Thin accessor over {#interface_definition} for the presence check.
1371
1770
  def interface_method_names(interface_name)
1372
1771
  interface_definition(interface_name)&.methods&.keys
1373
1772
  end
1374
1773
 
1375
- # @param rbs_alias [RBS::Types::Alias] a type-alias reference (`string`, `int`, `range[int?]`, …)
1774
+ # @param rbs_alias — a type-alias reference (`string`, `int`, `range[int?]`, …)
1376
1775
  # appearing in a method signature.
1377
- # @return [RBS::Types::t, nil] the alias's aliased type one level out, with type arguments substituted
1776
+ # @return the alias's aliased type one level out, with type arguments substituted
1378
1777
  # for a generic alias (`string` → `::String | ::_ToStr`; `range[int?]` → `::Range[int?] |
1379
1778
  # ::_Range[int?]`), or nil for an unresolved name. Lets a caller see through the alias that
1380
1779
  # {Inference::RbsTypeTranslator} otherwise degrades to `untyped`, which is why an interface/alias
@@ -1399,7 +1798,7 @@ module Rigor
1399
1798
  nil
1400
1799
  end
1401
1800
 
1402
- # @return [RBS::Definition, nil] the resolved singleton (class object) definition for `class_name`. The
1801
+ # @return the resolved singleton (class object) definition for `class_name`. The
1403
1802
  # methods on this definition are the *class methods* of `class_name`, including those inherited from
1404
1803
  # `Class` and `Module` for class types. Returns nil for unknown names and on RBS build errors
1405
1804
  # (fail-soft).
@@ -1420,7 +1819,37 @@ module Rigor
1420
1819
  definition
1421
1820
  end
1422
1821
 
1423
- # @return [RBS::Definition::Method, nil] the class method on `class_name`. For example,
1822
+ # Issue #915 — the MODULE names in `class_name`'s singleton ancestry, as RBS declares it. A
1823
+ # `class C; extend M; end` written in a project's own `sig/` puts `M` there and nothing else in the
1824
+ # environment surface exposes it: {#ancestor_names_for} answers the instance side only.
1825
+ #
1826
+ # Modules only, deliberately. `RBS::DefinitionBuilder#build_singleton` mixes the extended modules in
1827
+ # with the singleton's class chain (`Class`, `Module`, `Object`, `BasicObject`), and the classes there
1828
+ # are already what the `Class` approximation in `Inference::Narrowing` reasons through. The modules
1829
+ # that survive the filter — the declared `extend` targets, plus `Kernel` through `Object` — are each a
1830
+ # true answer to `C.is_a?(M)`.
1831
+ #
1832
+ # Marked as an internal demand for the same reason {#ancestor_names_for} is (issue #696): asking
1833
+ # whether a module sits in an ancestry is not asking whether the class's method surface resolves, and
1834
+ # letting it report would make the reported class list depend on the cache state.
1835
+ #
1836
+ # @return `::`-stripped module names, or `[]` for an unknown class and on any RBS build error.
1837
+ def singleton_extended_module_names(class_name)
1838
+ key = class_name.to_s.delete_prefix("::")
1839
+ during_internal_demand do
1840
+ definition = singleton_definition(key)
1841
+ next [].freeze if definition.nil?
1842
+
1843
+ definition.ancestors.ancestors.filter_map do |ancestor|
1844
+ name = ancestor.name.to_s.delete_prefix("::")
1845
+ name if ancestor.is_a?(::RBS::Definition::Ancestor::Instance) && rbs_module?(name)
1846
+ end.uniq.freeze
1847
+ end
1848
+ rescue ::RBS::BaseError, StandardError
1849
+ [].freeze
1850
+ end
1851
+
1852
+ # @return the class method on `class_name`. For example,
1424
1853
  # `singleton_method(class_name: "Integer", method_name: :sqrt)` returns the definition for
1425
1854
  # `Integer.sqrt`, while `singleton_method(class_name: "Foo", method_name: :new)` returns Class#new
1426
1855
  # for any class type.
@@ -1444,18 +1873,29 @@ module Rigor
1444
1873
  # process) through {Cache::RbsClassTypeParamNames.fetch} and answers point lookups from it. Cold runs
1445
1874
  # build the table once and persist it; warm runs (and a separate loader sharing the same Store) skip
1446
1875
  # the env walk entirely.
1876
+ #
1877
+ # #775 — memoised per class name and answered as a frozen list. `RBS::Definition#type_params` is not
1878
+ # an accessor: it re-derives the names through the entry's type-param declarations (a substitution
1879
+ # per call), and generic dispatch asks for the receiver class's names on every call (~110k times on
1880
+ # the lib self-check, the largest remaining rbs-side allocation). The env is immutable for the
1881
+ # loader's lifetime, so the answer never changes; every consumer only reads it.
1447
1882
  def class_type_param_names(class_name)
1448
- if cache_store
1449
- key = class_name.to_s.delete_prefix("::")
1450
- return type_param_names_table.fetch(key, []).dup
1451
- end
1452
-
1453
- definition = instance_definition(class_name)
1454
- return [] unless definition
1883
+ memo = (@state[:type_param_names] ||= {})
1884
+ key = class_name.to_s
1885
+ return memo[key] if memo.key?(key)
1455
1886
 
1456
- definition.type_params.dup
1887
+ memo[key] =
1888
+ if cache_store
1889
+ type_param_names_table.fetch(key.delete_prefix("::"), NO_TYPE_PARAMS).freeze
1890
+ else
1891
+ definition = instance_definition(class_name)
1892
+ definition ? definition.type_params.freeze : NO_TYPE_PARAMS
1893
+ end
1457
1894
  end
1458
1895
 
1896
+ NO_TYPE_PARAMS = [].freeze
1897
+ private_constant :NO_TYPE_PARAMS
1898
+
1459
1899
  def class_ordering(lhs, rhs)
1460
1900
  @hierarchy.class_ordering(lhs, rhs)
1461
1901
  end
@@ -1486,7 +1926,7 @@ module Rigor
1486
1926
  # way, and both yield `[]` for an unknown or unbuildable class. Pinned by spec across all three cache
1487
1927
  # states.
1488
1928
  #
1489
- # @return [Array<String>] `::`-stripped ancestor names, or `[]` for an unknown or unbuildable class.
1929
+ # @return `::`-stripped ancestor names, or `[]` for an unknown or unbuildable class.
1490
1930
  def ancestor_names_for(class_name)
1491
1931
  key = class_name.to_s.delete_prefix("::")
1492
1932
  during_internal_demand do
@@ -1501,7 +1941,7 @@ module Rigor
1501
1941
  [].freeze
1502
1942
  end
1503
1943
 
1504
- # @return [Array<String>] every RBS-declared constant name (top-level prefixed, e.g., `"::Math::PI"`)
1944
+ # @return every RBS-declared constant name (top-level prefixed, e.g., `"::Math::PI"`)
1505
1945
  # currently loaded into the environment. Used by the cache producer that materialises the
1506
1946
  # constant-type table; ordinary callers should keep using {#constant_type} for point lookups.
1507
1947
  def constant_names
@@ -1736,11 +2176,10 @@ module Rigor
1736
2176
  end
1737
2177
 
1738
2178
  # The RBS environment for this loader. Memoised both on success AND on failure: when the env build
1739
- # raises (typically `RBS::DuplicatedDeclarationError` because a `signature_paths:` entry redeclares a
1740
- # constant or class already shipped by stdlib RBS), retrying on every subsequent `env` call would
1741
- # re-parse and re-resolve the whole sig set per AST node touched during analysis, multiplying per-file
1742
- # analysis cost by ~100x. Failures short-circuit to `nil` here and are surfaced to the user via
1743
- # `warn_about_env_build_failure_once` so the broken `signature_paths:` entry is identifiable.
2179
+ # raises an *unrecoverable* `RBS::BaseError` (project-vs-bundled duplicates are quarantined per-file
2180
+ # since #777), retrying on every subsequent `env` call would re-parse and re-resolve the whole sig set
2181
+ # per AST node touched during analysis, multiplying per-file analysis cost by ~100x. Failures
2182
+ # short-circuit to `nil` here and are surfaced to the user via `warn_about_env_build_failure_once`.
1744
2183
  def env
1745
2184
  return @state[:env] if @state[:env_loaded]
1746
2185
 
@@ -1778,10 +2217,11 @@ module Rigor
1778
2217
  lines = listed.map { |_path, first_line| " - #{first_line}" }
1779
2218
  lines << " … and #{more} more" if more.positive?
1780
2219
  warn(
1781
- "rigor: skipped #{quarantined.size} unparseable RBS file(s) under `signature_paths:`.\n " \
1782
- "They were QUARANTINED so the rest of your RBS env still loads, but the types they\n " \
1783
- "declare are absent — calls into them read `Dynamic[top]`, so coverage and diagnostics\n " \
1784
- "are reduced. Fix the parse error(s) to restore that coverage:\n" \
2220
+ "rigor: skipped #{quarantined.size} RBS file(s) under `signature_paths:` (unparseable or\n " \
2221
+ "duplicated against bundled RBS). They were QUARANTINED so the rest of your RBS env\n " \
2222
+ "still loads, but the types they declare are absent — calls into them read\n " \
2223
+ "`Dynamic[top]`, so coverage and diagnostics are reduced. Fix the parse error(s) or\n " \
2224
+ "remove the conflicting declaration(s) to restore that coverage:\n" \
1785
2225
  "#{lines.join("\n")}"
1786
2226
  )
1787
2227
  end
@@ -1870,8 +2310,8 @@ module Rigor
1870
2310
  #
1871
2311
  # {#during_internal_demand} (issue #696) — these two walk EVERY known class, so a failure they hit is a
1872
2312
  # failure of the sig set, not of anything the run asked about, and it must not reach
1873
- # {#definition_build_failures}. The stderr banner is deliberately left armed here, byte-for-byte as
1874
- # before.
2313
+ # {#definition_build_failures}. The stderr fallback stays armed, but {#during_internal_demand} folds
2314
+ # every failure found by one outermost walk into one bounded summary (issue #718).
1875
2315
  def instance_definitions_table
1876
2316
  @state[:instance_definitions_table] ||= during_internal_demand do
1877
2317
  build_definitions_table { |name| build_instance_definition(name) }
@@ -2005,7 +2445,9 @@ module Rigor
2005
2445
  # review, second pass).
2006
2446
  #
2007
2447
  # Save-and-restore rather than a bare flag: `#prewarm` wraps a body whose members wrap themselves, and
2008
- # a nested demand must not un-mark its caller on the way out.
2448
+ # a nested demand must not un-mark its caller on the way out. A successful outermost exit flushes the
2449
+ # deferred stderr summary; an outermost exit that raises keeps the detailed fallback banner armed even
2450
+ # when an earlier internal-demand episode already emitted the loader-instance summary.
2009
2451
  #
2010
2452
  # Per LOADER, not per thread. Nesting and a raise mid-demand are both handled, and the fork pool forks
2011
2453
  # after `#prewarm` returns, so no CLI path shares a loader across concurrent analyses. An in-process
@@ -2013,10 +2455,29 @@ module Rigor
2013
2455
  # silence another's reporting; not reachable today, and not worth a thread-local until it is.
2014
2456
  def during_internal_demand
2015
2457
  previous = @state[:internal_demand]
2458
+ outermost = !previous
2459
+ if outermost
2460
+ @state[:definition_build_deferred_count] = 0
2461
+ @state[:definition_build_deferred_first] = nil
2462
+ end
2016
2463
  @state[:internal_demand] = true
2017
- yield
2018
- ensure
2019
- @state[:internal_demand] = previous
2464
+ # `rescue`/`else` keeps the normal block result precise for the analyzer; the pending marker covers
2465
+ # non-local exits (e.g. `throw`) that unwind through `ensure` without entering either branch.
2466
+ @state[:internal_demand_status] = :pending if outermost
2467
+ begin
2468
+ result = yield
2469
+ @state[:internal_demand_status] = :completed if outermost
2470
+ rescue StandardError, ScriptError => e
2471
+ @state[:internal_demand_status] = :aborted if outermost
2472
+ warn_about_aborted_definition_build_failures if outermost
2473
+ raise e
2474
+ else
2475
+ warn_about_deferred_definition_build_failures if outermost
2476
+ result
2477
+ ensure
2478
+ @state[:internal_demand] = previous
2479
+ warn_about_aborted_definition_build_failures if outermost && @state[:internal_demand_status] == :pending
2480
+ end
2020
2481
  end
2021
2482
 
2022
2483
  # The third twin of {#warn_about_quarantined_signatures} / {#warn_about_virtual_rbs_collisions}: name,
@@ -2029,21 +2490,33 @@ module Rigor
2029
2490
  # {#env}, because the whole env is built eagerly and every quarantine/collision is already known by
2030
2491
  # then. Definition builds are LAZY (ADR-54 WD1 — built on demand per class the FIRST time a caller asks,
2031
2492
  # long after {#env} has already run), so there is no later central checkpoint to fire from before the
2032
- # affected classes even exist. This warns inline at the rescue site instead, gated on
2033
- # `@state[:definition_build_warned]` (keyed by class name) so the instance and singleton sides — and
2034
- # any re-entry once the per-process `@instance_definition_cache` / `@singleton_definition_cache`
2035
- # memoize the failure — warn at most once per class name, cache-hit runs included (a definition build
2036
- # is per-process regardless of the RBS-env cache tier). `@state` is per-LOADER-INSTANCE, not
2037
- # process-global, so under the fork-based analysis pool each worker holds its own loader and its own
2038
- # `@state`: a class whose definition fails can print its warning once per worker that happens to touch
2039
- # it, i.e. more than once in a single `rigor check` run. Deduplicating that across processes is out of
2040
- # scope here — see [#295](https://github.com/rigortype/rigor/issues/295).
2493
+ # affected classes even exist. An ordinary demand therefore warns inline at the rescue site; Rigor's
2494
+ # own internal demand defers until its outermost boundary and collapses the whole walk to one summary
2495
+ # (issue #718). Both routes share `@state[:definition_build_warned]` (keyed by normalized class name),
2496
+ # so the instance and singleton sides — and any re-entry once the per-loader-instance definition caches memoize
2497
+ # the failure — count or warn at most once per class name, cache-hit runs included.
2498
+ #
2499
+ # `@state` is per LOADER INSTANCE, not process-global. A fork-pool prewarm finishes before the fork, so
2500
+ # that loader instance emits its summary once in the parent and its dedupe state is inherited by the
2501
+ # workers. A separate loader instance (for example, a parameter-inference pre-pass) has its own summary
2502
+ # budget. A failure first reached by an ordinary demand can still warn once in each worker that reaches
2503
+ # it; deduplicating that across processes is out of scope here — see [#295](https://github.com/rigortype/rigor/issues/295).
2041
2504
  def warn_about_definition_build_failure(class_name, error)
2042
2505
  warned = (@state[:definition_build_warned] ||= {})
2043
- key = class_name.to_s
2506
+ key = class_name.to_s.delete_prefix("::")
2044
2507
  return if warned[key]
2045
2508
 
2046
2509
  warned[key] = true
2510
+ if @state[:internal_demand]
2511
+ @state[:definition_build_deferred_count] = @state.fetch(:definition_build_deferred_count, 0) + 1
2512
+ @state[:definition_build_deferred_first] ||= [class_name.to_s, error]
2513
+ return
2514
+ end
2515
+
2516
+ warn(definition_build_failure_warning(class_name, error))
2517
+ end
2518
+
2519
+ def definition_build_failure_warning(class_name, error)
2047
2520
  first_line = error.message.to_s.lines.first.to_s.strip
2048
2521
  buffers = definition_build_conflict_buffers(error)
2049
2522
  collisions =
@@ -2056,13 +2529,42 @@ module Rigor
2056
2529
  lines << " … and #{more} more" if more.positive?
2057
2530
  "\n Colliding declaration(s):\n#{lines.join("\n")}"
2058
2531
  end
2059
- warn(
2060
- "rigor: RBS definition build failed for `#{class_name}`: #{error.class}: #{first_line}\n " \
2532
+ "rigor: RBS definition build failed for `#{class_name}`: #{error.class}: #{first_line}\n " \
2061
2533
  "Rigor still treats the class as known, so calls into it now silently degrade to\n " \
2062
2534
  "`Dynamic[top]` — real methods and typos alike — instead of resolving normally.#{collisions}"
2535
+ end
2536
+
2537
+ def warn_about_deferred_definition_build_failures
2538
+ return if @state[:definition_build_summary_warned]
2539
+
2540
+ count = @state[:definition_build_deferred_count]
2541
+ return if count.nil? || count.zero?
2542
+
2543
+ @state[:definition_build_summary_warned] = true
2544
+ first_class, first_error = @state[:definition_build_deferred_first]
2545
+ affected = if count == 1
2546
+ "1 class affected"
2547
+ else
2548
+ more = count - 1
2549
+ noun = more == 1 ? "class" : "classes"
2550
+ "… and #{more} more #{noun} affected (#{count} total)"
2551
+ end
2552
+ warn(
2553
+ "#{definition_build_failure_warning(first_class, first_error)}\n " \
2554
+ "Internal whole-universe demand found #{affected}; the " \
2555
+ "`rbs.coverage.definition-build-failed` diagnostic reports the classes the analysis demanded and " \
2556
+ "the culprit member and signature files."
2063
2557
  )
2064
2558
  end
2065
2559
 
2560
+ def warn_about_aborted_definition_build_failures
2561
+ count = @state[:definition_build_deferred_count]
2562
+ return if count.nil? || count.zero?
2563
+
2564
+ first_class, first_error = @state[:definition_build_deferred_first]
2565
+ warn(definition_build_failure_warning(first_class, first_error))
2566
+ end
2567
+
2066
2568
  # The definition-build twin of {#env_build_conflict_buffers}: the declaration source file(s) named by a
2067
2569
  # `RBS::DefinitionBuilder` failure, so {#warn_about_definition_build_failure} can name the colliding
2068
2570
  # declarations rather than only the exception's class and message. The payload shape varies by error
@@ -2105,8 +2607,6 @@ module Rigor
2105
2607
  # `#method_name`; `UnknownMethodAliasError` and `NoSuperclassFoundError` have `#type_name`;
2106
2608
  # `SuperclassMismatchError` has `#name`. `RecursiveAncestorError` has none of them and yields nil, and
2107
2609
  # the diagnostic then omits the clause rather than inventing one.
2108
- #
2109
- # @return [String, nil]
2110
2610
  def definition_build_member(error)
2111
2611
  return error.qualified_method_name.to_s if error.respond_to?(:qualified_method_name)
2112
2612