rigortype 0.4.5 → 0.4.7

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 (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -28,8 +28,8 @@ module Rigor
28
28
  #
29
29
  # An `ActiveRecord::Base` row reaches `User.find` through the project's own
30
30
  # `User < ApplicationRecord < ActiveRecord::Base` lines, so one row covers every model in the app.
31
- # That is the whole economy of the framework layer: thirty-odd rows here colour the several thousand
32
- # database touches a Rails app performs, and none of them needed a line of application code.
31
+ # That is the whole economy of the framework layer: a fixed set of rows here colours the several
32
+ # thousand database touches a Rails app performs, and none of them needed a line of application code.
33
33
  module Effects
34
34
  BASE = "ActiveRecord::Base"
35
35
  RELATION = "ActiveRecord::Relation"
@@ -186,27 +186,20 @@ module Rigor
186
186
  merge_redeclarations(rows)
187
187
  end
188
188
 
189
- # Collapses the rows that declare the SAME constant into one, so {ModelIndex.build} — which keys
190
- # its entries by `class_name` — receives at most one row per model.
189
+ # Collapses the rows that declare the SAME constant into one, so {ModelIndex.build} (keyed by
190
+ # `class_name`) receives at most one row per model.
191
191
  #
192
- # A model is routinely declared more than once: `app/models/user.rb` holds `class User <
193
- # ApplicationRecord` and a second file reopens it (`class ::User`, `class User`, or a full
194
- # `class User < ApplicationRecord` redeclaration) to add a method. Ruby's own semantics are
195
- # ADDITIVE — a reopen contributes what it spells and leaves the rest of the class alone — so the
196
- # rows are UNIONed here. Taking the last row instead dropped the real declaration's associations,
197
- # scopes, enums, validations, callbacks and `alias_attribute`s whenever the reopen sorted later in
198
- # the glob, and `where(<a declared alias>: …)` then surfaced a false `unknown-column` on correct
199
- # code; taking the first dropped whatever the reopen added. Neither order loses anything now.
192
+ # A model is routinely declared more than once (`app/models/user.rb` plus a reopening file). Ruby's
193
+ # semantics are ADDITIVE, so the rows are UNIONed. Taking the last row dropped the real declaration's
194
+ # associations, scopes, enums, validations, callbacks and `alias_attribute`s whenever the reopen sorted
195
+ # later, producing false `unknown-column`s; taking the first dropped what the reopen added.
200
196
  #
201
- # Merge is by field: the first non-nil `superclass_name` wins (a second declaration is the same
202
- # thing, or invalid Ruby); `self.table_name =` is an ASSIGNMENT, so the LATER declaration's
203
- # `table_name_override` wins as it does at load time, and when the two declarations disagree the
204
- # resolved name is marked computed so no value is pinned — the glob order is not the load order for
205
- # two full declarations, and a pinned wrong literal folds `Model.table_name == "…"` on correct code;
206
- # `table_name_computed` is otherwise an OR (any computed name in the class makes the resolved one
207
- # inexact); name-keyed rows (associations, enums, aliases) let the LAST declaration override an
208
- # earlier same-name row exactly as {ModelIndex.merge_named_rows} does across an STI chain, and the
209
- # plain lists union.
197
+ # Merge is by field: the first non-nil `superclass_name` wins. `self.table_name =` is an ASSIGNMENT, so
198
+ # the later `table_name_override` wins, but when two declarations disagree the name is marked computed
199
+ # so no value is pinned: glob order is not load order, and a pinned wrong literal would fold
200
+ # `Model.table_name == "…"` on correct code. `table_name_computed` is otherwise an OR. Name-keyed rows
201
+ # let the LAST declaration override an earlier same-name row, as {ModelIndex.merge_named_rows} does
202
+ # across an STI chain; plain lists union.
210
203
  def merge_redeclarations(rows)
211
204
  return rows if rows.length < 2
212
205
 
@@ -459,7 +452,6 @@ module Rigor
459
452
  macro_methods: lookup_macro_methods(node.body)
460
453
  })
461
454
 
462
- # Recurse into the body in case nested classes exist.
463
455
  walk_for_classes(node.body, [full_name], &) if node.body
464
456
  end
465
457
 
@@ -869,8 +861,8 @@ module Rigor
869
861
 
870
862
  # Walks the namespace body's TOP-LEVEL statements only — a decorator declared inside a nested
871
863
  # `module`/`class` belongs to THAT namespace, not this one, and must not be attributed here.
872
- # Returns `Hash<:prefix|:suffix => { literal: } | { computed: true }>`, only for the names this body
873
- # actually mentions. A later statement overrides an earlier one within the SAME body — real Ruby
864
+ # Returns the decorators keyed by `:prefix` / `:suffix`, only for the names this body actually mentions.
865
+ # A later statement overrides an earlier one within the SAME body — real Ruby
874
866
  # assignment order, not a guess: `mattr_accessor :table_name_prefix` (no `default:`, so `computed:
875
867
  # true`) followed two lines down by a real `self.table_name_prefix = "blog_"` folds to the literal.
876
868
  #
@@ -1234,8 +1226,8 @@ module Rigor
1234
1226
  end
1235
1227
 
1236
1228
  # Recognised single-instance and collection association DSL methods. The kind drives the eventual
1237
- # return-type contribution: singular associations narrow to `Nominal[Target] | nil`, plural ones
1238
- # narrow to `ActiveRecord::Relation[Target]`.
1229
+ # return-type contribution: singular associations narrow to the target or nil, plural ones to a
1230
+ # relation of the target.
1239
1231
  #
1240
1232
  # `composed_of` value-object aggregations and `delegated_type` roles are folded in here too — both
1241
1233
  # accept the association name as a `where` / `find_by` query key, so omitting them turns every
@@ -1336,7 +1328,7 @@ module Rigor
1336
1328
  association_option(args, "polymorphic") == true
1337
1329
 
1338
1330
  # A polymorphic association has no single static target class — `target` is nil and the flow
1339
- # contribution declines to narrow rather than inventing a wrong `Nominal[<classified-name>]`.
1331
+ # contribution declines to narrow rather than inventing a wrong class-name target.
1340
1332
  if polymorphic
1341
1333
  target = nil
1342
1334
  else
@@ -1460,8 +1452,7 @@ module Rigor
1460
1452
  end
1461
1453
 
1462
1454
  # `enum status: { active: 0, archived: 1 }` (Rails ≤6) and `enum :status, [:active, :archived]`
1463
- # (Rails 7+). Returns `Hash<column_name => Array<Symbol>>`. Non-literal forms decline rather than
1464
- # guess.
1455
+ # (Rails 7+). Non-literal forms decline rather than guess.
1465
1456
  def lookup_enums(body)
1466
1457
  return {} if body.nil?
1467
1458
 
@@ -1516,7 +1507,7 @@ module Rigor
1516
1507
  end
1517
1508
 
1518
1509
  # `scope :active, -> { ... }`. Records the scope name only (the body is intentionally NOT
1519
- # introspected — the caller contributes `ActiveRecord::Relation[Model]` based on the name alone
1510
+ # introspected — the caller contributes a relation of the model based on the name alone
1520
1511
  # via `class_scope_return_type`).
1521
1512
  def lookup_scopes(body)
1522
1513
  return [] if body.nil?
@@ -1585,8 +1576,7 @@ module Rigor
1585
1576
 
1586
1577
  # `alias_attribute :new_name, :old_name`. Records the mapping so the analyzer accepts the alias
1587
1578
  # as a query key — without it every `where(<alias>: ...)` / `find_by(<alias>: ...)` call surfaces
1588
- # as a false `unknown-column`. Returns `Hash<alias => target>`; non-Symbol-literal forms decline
1589
- # rather than guess.
1579
+ # as a false `unknown-column`. Non-Symbol-literal forms decline rather than guess.
1590
1580
  def lookup_aliases(body)
1591
1581
  return {} if body.nil?
1592
1582
 
@@ -1626,7 +1616,7 @@ module Rigor
1626
1616
  # macro and `images`, `images=`, `images_attachments`, `images_blobs` for the collection one — the
1627
1617
  # `has_one_attached` / `has_many_attached` generated-methods surface, none of it column-backed.
1628
1618
  # This is name MEMBERSHIP only, and does not overlap `rigor-activestorage`, which contributes the
1629
- # accessor's TYPE (`ActiveStorage::Attached::One`) and publishes no fact: a consumer that reads
1619
+ # accessor's type and publishes no fact: a consumer that reads
1630
1620
  # `:model_index` to ask "does this model answer `avatar`?" gets nothing from that plugin, and gets
1631
1621
  # nothing at all when the project does not load it.
1632
1622
  ATTACHMENT_MACROS = {
@@ -29,14 +29,12 @@ module Rigor
29
29
  # {Entry} resolves its table from the root model and inherits the chain's declared associations /
30
30
  # enums / aliases / scopes / validations / callbacks / macro methods.
31
31
  class ModelIndex
32
- # `associations` is a frozen `Array<Hash>` where each row carries `{ name:, kind:, target:,
33
- # nullable: }`:
32
+ # `associations` is a frozen list of rows `{ name:, kind:, target:, nullable: }`:
34
33
  #
35
- # - `name` — String, the association method name as the user invokes it (`"posts"`).
34
+ # - `name` — the association method name as the user invokes it (`"posts"`).
36
35
  # - `kind` — `:singular` (`belongs_to` / `has_one`) or `:collection` (`has_many`).
37
- # - `target` — String, the target class name resolved either from an explicit `class_name:`
38
- # option or via {Inflector.classify}.
39
- # - `nullable` — Boolean; whether a `:singular` accessor can return `nil`. `has_one` → `true`;
36
+ # - `target` — the target class name, from an explicit `class_name:` option or via {Inflector.classify}.
37
+ # - `nullable` — whether a `:singular` accessor can return `nil`. `has_one` → `true`;
40
38
  # `belongs_to` → `false` (required by default since Rails 5) unless `optional: true`
41
39
  # / `required: false`. Meaningless for `:collection` rows.
42
40
  #
@@ -71,27 +69,23 @@ module Rigor
71
69
 
72
70
  def association_names = associations.map { |a| a[:name] }
73
71
 
74
- # `enums` is `Hash<column_name => Array<value_name>>`.
72
+ # `enums` maps a column name to its value names.
75
73
  def enum?(name) = enums.key?(name.to_s)
76
74
  def enum_values(name) = enums.fetch(name.to_s, [])
77
75
 
78
- # `scopes` is `Array<scope_name>`.
79
76
  def scope?(name) = scopes.include?(name.to_s)
80
77
 
81
- # `validations` is `Array<attribute_name>` covering both `validates :name, ...` and the
82
- # `validates_*_of :name, ...` shorthand families.
78
+ # `validations` covers both `validates :name, ...` and the `validates_*_of :name, ...` shorthand families.
83
79
  def validation?(name) = validations.include?(name.to_s)
84
80
  def validated_attributes = validations
85
81
 
86
- # `callbacks` is `Array<{ name:, callback: }>`.
87
82
  def callback_targets = callbacks.map { |c| c[:name] }
88
83
 
89
- # `aliases` is `Hash<alias_name => target_attribute>` populated from `alias_attribute`
90
- # declarations.
84
+ # `aliases` maps an alias name to its target attribute, from `alias_attribute` declarations.
91
85
  def alias?(name) = aliases.key?(name.to_s)
92
86
  def resolve_alias(name) = aliases[name.to_s]
93
87
 
94
- # `macro_methods` is `Array<method_name>` — the instance methods a declaration macro installs that
88
+ # `macro_methods` lists the instance methods a declaration macro installs that
95
89
  # none of the sets above already carries (#1049): `delegate`'s names, the Paperclip / Active
96
90
  # Storage attachment readers, and an `enum`'s per-value predicates. Names only, with no type: a
97
91
  # consumer asks "does this model answer `followers_count`?", which is what a fail-closed check
@@ -312,7 +306,7 @@ module Rigor
312
306
  seen.values.map(&:freeze).freeze
313
307
  end
314
308
 
315
- # `Hash<column => Array<value>>` merged across the STI chain; a child redeclaration of the same
309
+ # Enum value lists merged across the STI chain; a child redeclaration of the same
316
310
  # enum column overrides the ancestor's value list.
317
311
  def self.merge_enums(chain)
318
312
  chain.each_with_object({}) do |row, acc|
@@ -320,7 +314,7 @@ module Rigor
320
314
  end.freeze
321
315
  end
322
316
 
323
- # `Hash<alias => target>` merged across the STI chain.
317
+ # Alias map merged across the STI chain.
324
318
  def self.merge_aliases(chain)
325
319
  chain.each_with_object({}) do |row, acc|
326
320
  (row[:aliases] || {}).each { |name, target| acc[name.to_s] = target.to_s }
@@ -108,9 +108,8 @@ module Rigor
108
108
  false
109
109
  end
110
110
 
111
- # Walks the block body collecting `t.<method>(...)` calls. Skips nested blocks (e.g. inside
112
- # `if`-conditioned columns) only at the top level — for richer schema constructs the parser falls
113
- # back silently.
111
+ # Walks the block body collecting `t.<method>(...)` calls. It descends through nested constructs
112
+ # (e.g. `if` branches) but does not recurse into a matched call.
114
113
  def collect_column_calls(node, &)
115
114
  return if node.nil?
116
115
 
@@ -21,7 +21,6 @@ module Rigor
21
21
  end
22
22
  end
23
23
 
24
- # Map ActiveRecord column types → Ruby class names.
25
24
  RUBY_TYPE_MAPPING = {
26
25
  string: "String",
27
26
  text: "String",
@@ -59,8 +59,8 @@ module Rigor
59
59
  # capital R) is intentional — keeps the constant lookup distinct from `::ActiveRecord` even though the
60
60
  # gem name is hyphenated.
61
61
  #
62
- # Note: this plugin is the seventh worked example. It does NOT require `active_record` at runtime — it
63
- # only reads project source, the same way the other examples do. Rigor stays decoupled from Rails.
62
+ # Note: this plugin does NOT require `active_record` at runtime — it only reads project source, the
63
+ # same way the other examples do. Rigor stays decoupled from Rails.
64
64
  class Activerecord < Rigor::Plugin::Base
65
65
  manifest(
66
66
  id: "activerecord",
@@ -553,8 +553,9 @@ module Rigor
553
553
  end
554
554
 
555
555
  # Class-side finders + the class-side relation entry points. `find` returns the model, or an Array of
556
- # them for two or more ids, or with a block the model or `nil`; `find_by!` returns the model; `find_by` adds the `nil` arm; `where` /
557
- # `all` / `order` / `limit` / `none` open a relation. The relation then carries its element type
556
+ # them for two or more ids, or with a block the model or `nil`; `find_by!` returns the model;
557
+ # `find_by` adds the `nil` arm; `where` / `all` / `order` / `limit` / `none` open a relation. The
558
+ # relation then carries its element type
558
559
  # through any further chained query method via the bundled `ActiveRecord::Relation` RBS.
559
560
  def finder_return_type(call_node, entry, scope)
560
561
  case call_node.name
@@ -105,10 +105,9 @@ module Rigor
105
105
  walk(node.body, inner_path, &) if node.body
106
106
  end
107
107
 
108
- # The full constant name a `class` / `module` declaration defines, given the rendered local name
109
- # and the enclosing lexical path. A ROOTED local name (`class ::User`) names the top-level constant
110
- # whatever the nesting, so the lexical path is dropped and the `::` with it; otherwise the name is
111
- # appended to the path (`class User` inside `module Admin` → `Admin::User`).
108
+ # The full constant name a `class` / `module` declaration defines. A ROOTED local name (`class ::User`)
109
+ # names the top-level constant whatever the nesting, so the lexical path is dropped; otherwise the
110
+ # name is appended to it (`class User` inside `module Admin` → `Admin::User`).
112
111
  def declared_constant_name(local_name, lexical_path)
113
112
  return local_name.delete_prefix("::") if local_name.start_with?("::")
114
113
 
@@ -8,7 +8,7 @@ module Rigor
8
8
  #
9
9
  # Each row is `{ name:, kind: }`:
10
10
  #
11
- # - `name` — String, the attachment method name as the user invokes it (`"avatar"`).
11
+ # - `name` — the attachment method name as the user invokes it (`"avatar"`).
12
12
  # - `kind` — `:singular` (`has_one_attached`) or `:collection` (`has_many_attached`).
13
13
  class AttachmentIndex
14
14
  attr_reader :entries
@@ -137,19 +137,14 @@ module Rigor
137
137
  nil
138
138
  end
139
139
 
140
- # Issue #1056 — "the attachment index did not load" is a fact about the run's INPUTS, not about the
141
- # file being analysed, so both outcomes are registered rather than returned: see
142
- # {Plugin::Base#disclose_once} for the channel and the position. Returned from the per-file hook they
143
- # carried no once-guard at all — the `.uniq` collapsed repeats within one instance's list, but the
144
- # whole list was re-emitted on every analysed file.
140
+ # Issue #1056 — "the attachment index did not load" is a fact about the run's INPUTS, not the file
141
+ # being analysed, so it is registered via {Plugin::Base#disclose_once} rather than returned from the
142
+ # per-file hook (which had no once-guard and re-emitted the list on every file).
145
143
  #
146
- # The `key`s carry an ordinal prefix because the engine emits a plugin's disclosures in KEY order
147
- # (#1051), and a refused read must still precede a discovery failure the way `@load_errors` records
148
- # them — `@load_errors` is now keyed by that same string, which also caps it: `#attachment_index`
149
- # re-attempts the load per call site, and the old Array appended a fresh interpolated string every
150
- # time (the unbounded-growth shape #569 measured at 4.2 M retained strings on Redmine). The key, not
151
- # the message, is the identity: a second refusal naming a different path collapses into the first row
152
- # rather than adding one — the notice is "the index did not load", said once.
144
+ # The `key`s carry an ordinal prefix because the engine emits disclosures in KEY order (#1051), and
145
+ # a refused read must precede a discovery failure. `@load_errors` is keyed by the same string, which
146
+ # caps it: `#attachment_index` re-attempts the load per call site (#569). The key, not the message,
147
+ # is the identity, so a second refusal collapses into the first row.
153
148
  def disclose_load_errors
154
149
  @load_errors.each do |key, message|
155
150
  disclose_once(key, message: message, severity: :warning, rule: "load-error")
@@ -3,4 +3,4 @@
3
3
  # Entry point matching the `rigor-<id>` gem-name convention. The Plugin::Loader's `require_gem!` step
4
4
  # calls `require "rigor-activestorage"`, and this file loads the plugin's actual implementation under
5
5
  # the `rigor/plugin/` namespace so the require / register pair fires.
6
- require "rigor/plugin/activestorage"
6
+ require_relative "rigor/plugin/activestorage"
@@ -12,7 +12,7 @@ module Rigor
12
12
  # the core_ext predicate surface — the single cheapest purity win in a Rails app, per WD10 — is issue
13
13
  # #388 and lands in this plugin's shipped RBS, not here (`try` was audited and skipped: it dispatches
14
14
  # to whatever method the caller names, so its purity is a fact about the call site, not about `try`).
15
- # One row below (`in_time_zone_row`) is #388's own addition: `DateTime#in_time_zone` reads `Time.zone`
15
+ # The `in_time_zone_rows` group is #388's own addition: `DateTime#in_time_zone` reads `Time.zone`
16
16
  # through a default argument, a gap the RBS sweep found but can only fix here.
17
17
  #
18
18
  # ## The clock
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rigor/type"
4
+
5
+ module Rigor
6
+ module Plugin
7
+ class ActivesupportCoreExt < Rigor::Plugin::Base
8
+ # #2433 — the result type of `Hash#symbolize_keys` / `#to_options`.
9
+ #
10
+ # ActiveSupport implements them as `transform_keys { |key| key.to_sym rescue key }`, so a String or
11
+ # Symbol key becomes a Symbol and a key without `to_sym` (an Integer, a Float) survives unchanged. One
12
+ # RBS return type cannot say that: `Hash[Symbol, V]` reads an Integer key as a Symbol, and
13
+ # `Hash[Symbol | K, V]` makes a String-keyed hash fail a declared `Hash[Symbol, untyped]` parameter.
14
+ # So the RBS rows answer `Hash[untyped, V]` and this module maps each key member where it can.
15
+ #
16
+ # A key that is not a String or Symbol (an Integer, a project class) reads `Dynamic[top]`: no Rigor fact proves
17
+ # a class has no `to_sym` (a mixin, a `pre_eval` file or a reopening can add one), so a kept key could claim
18
+ # a type the program does not have. It declines (`nil`, the RBS row answers) when a receiver member is
19
+ # neither a `Hash[K, V]` nor `nil`. A `nil` member is dropped: `nil.symbolize_keys` raises, and the engine
20
+ # adds `nil` back for `&.`.
21
+ module SymbolizeKeys
22
+ HASH = "Hash"
23
+ SYMBOLIZED_CLASSES = %w[String Symbol].freeze
24
+
25
+ module_function
26
+
27
+ # Answers `Hash[K', V]` (the union of such over the receiver's Hash members), or nil to decline.
28
+ def result(receiver_type, scope)
29
+ members = receiver_type.is_a?(Rigor::Type::Union) ? receiver_type.members : [receiver_type]
30
+ answers = members.reject { |member| nil_member?(member) }.map { |member| member_answer(member, scope) }
31
+ return nil if answers.empty? || answers.any?(:decline)
32
+
33
+ hashes = answers.reject { |answer| answer == :empty }
34
+ hashes.empty? ? nil : Rigor::Type::Combinator.union(*hashes)
35
+ end
36
+
37
+ # An empty closed shape is `{}`: it adds no key, so it drops out of the union rather than declining it.
38
+ def member_answer(member, scope)
39
+ return :empty if member.is_a?(Rigor::Type::HashShape) && member.closed? && member.pairs.empty?
40
+
41
+ key_type, value_type = hash_arguments(member)
42
+ return :decline if key_type.nil?
43
+
44
+ Rigor::Type::Combinator.nominal_of(HASH, type_args: [map_key(key_type, scope), value_type])
45
+ end
46
+
47
+ def nil_member?(type)
48
+ (type.is_a?(Rigor::Type::Constant) && type.value.nil?) ||
49
+ (type.is_a?(Rigor::Type::Nominal) && type.class_name == "NilClass")
50
+ end
51
+
52
+ def hash_arguments(type)
53
+ case type
54
+ when Rigor::Type::Nominal
55
+ type.class_name == HASH && type.type_args.size == 2 ? type.type_args : nil
56
+ when Rigor::Type::Difference
57
+ hash_arguments(type.base)
58
+ when Rigor::Type::HashShape
59
+ shape_arguments(type)
60
+ end
61
+ end
62
+
63
+ # A closed, non-empty shape reads as `Hash[<its keys>, <union of its values>]`.
64
+ def shape_arguments(shape)
65
+ return nil if shape.open? || shape.pairs.empty?
66
+
67
+ keys = shape.pairs.keys.map { |key| Rigor::Type::Combinator.constant_of(key) }
68
+ [Rigor::Type::Combinator.union(*keys), Rigor::Type::Combinator.union(*shape.pairs.values)]
69
+ end
70
+
71
+ def map_key(type, scope)
72
+ members = type.is_a?(Rigor::Type::Union) ? type.members : [type]
73
+ Rigor::Type::Combinator.union(*members.map { |member| map_member(member, scope) })
74
+ end
75
+
76
+ def map_member(member, scope)
77
+ case member
78
+ when Rigor::Type::Constant
79
+ member.value.is_a?(String) || member.value.is_a?(Symbol) ? symbol : Rigor::Type::Combinator.untyped
80
+ when Rigor::Type::Nominal
81
+ symbolized_class?(member.class_name, scope) ? symbol : Rigor::Type::Combinator.untyped
82
+ else Rigor::Type::Combinator.untyped
83
+ end
84
+ end
85
+
86
+ def symbol
87
+ Rigor::Type::Combinator.nominal_of("Symbol")
88
+ end
89
+
90
+ # String, Symbol and their subclasses go through `to_sym`.
91
+ def symbolized_class?(name, scope)
92
+ SYMBOLIZED_CLASSES.include?(name) ||
93
+ SYMBOLIZED_CLASSES.any? { |base| %i[equal subclass].include?(ordering(name, base, scope)) }
94
+ end
95
+
96
+ def ordering(name, base, scope)
97
+ Rigor::Reflection.class_ordering(name, base, scope: scope)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -3,6 +3,7 @@
3
3
  require "rigor/plugin"
4
4
 
5
5
  require_relative "activesupport_core_ext/effects"
6
+ require_relative "activesupport_core_ext/symbolize_keys"
6
7
 
7
8
  module Rigor
8
9
  module Plugin
@@ -43,12 +44,15 @@ module Rigor
43
44
  manifest(
44
45
  id: "activesupport-core-ext",
45
46
  target_gems: ["activesupport"],
47
+ # Bumped 2026-10-27 (#2433) — `Hash#symbolize_keys` / `#to_options` map each key member through a
48
+ # `dynamic_return` hook instead of claiming `Hash[Symbol, V]`.
49
+ #
46
50
  # Bumped 2026-09-10 (#534 item 7) — `ActiveSupport::Concern` is declared, so `extend
47
51
  # ActiveSupport::Concern` resolves instead of reading as an unresolved constant.
48
52
  #
49
53
  # Bumped 2026-09-02 (#632) — `ActiveSupport::Duration`'s reader surface (`ago`/`to_i`/`iso8601`/…)
50
54
  # is now declared, open_receivers-protected; see the class comment above.
51
- version: "0.5.0",
55
+ version: "0.6.0",
52
56
  description: "RBS bundle for the most-frequently-flagged ActiveSupport core_ext extensions, " \
53
57
  "plus the `ActiveSupport::Duration` type of the numeric time multipliers and its " \
54
58
  "reader surface.",
@@ -190,11 +194,9 @@ module Rigor
190
194
  argument = duration_arithmetic_argument(call_node)
191
195
  next nil if argument.nil?
192
196
 
193
- # The ARGUMENT is checked before the receiver: it is the cheaper of the two on the overwhelmingly
194
- # common shapes (`i + 1`, `n * 2`), and a call with no Duration operand must cost as little as
195
- # possible — this rule is consulted on every `+` / `-` / `*` in the project. The early exit is
196
- # what makes the ordering worth anything: an argument that is no Duration operand at all
197
- # (`"a" + b`, `list + other`) declines here and never types its receiver on this rule's behalf.
197
+ # The ARGUMENT is checked before the receiver: it is the cheaper test on the common shapes (`i + 1`,
198
+ # `n * 2`), and this rule is consulted on every `+` / `-` / `*`. An argument that is no Duration
199
+ # operand (`"a" + b`, `list + other`) declines here and never types its receiver on this rule's behalf.
198
200
  argument_kind = duration_operand_kind(scope&.type_of(argument))
199
201
  next nil if argument_kind.nil?
200
202
 
@@ -204,6 +206,17 @@ module Rigor
204
206
  duration_arithmetic_result(call_node.name, receiver_kind, argument_kind)
205
207
  end
206
208
 
209
+ # #2433 — `symbolize_keys` / `to_options` keep a key without `to_sym`, so the key type is mapped per
210
+ # member; see {SymbolizeKeys}. The RBS rows answer `Hash[untyped, V]` wherever this declines.
211
+ dynamic_return methods: %i[symbolize_keys to_options] do |call_node, scope|
212
+ next nil unless call_node.is_a?(Prism::CallNode)
213
+ next nil unless call_node.receiver
214
+ next nil unless call_node.arguments.nil?
215
+
216
+ # A block is ignored by `symbolize_keys`, so it does not decline.
217
+ SymbolizeKeys.result(scope&.type_of(call_node.receiver), scope)
218
+ end
219
+
207
220
  # ADR-88 WD1 — both tables are static and the RBS bundle ships with the gem; nothing here reads a
208
221
  # project file, so a project enabling this plugin stays incremental-capable.
209
222
  def incremental_state_fingerprint
@@ -1565,28 +1565,35 @@ class Hash[unchecked out K, unchecked out V]
1565
1565
  # non-bang half is `%a{pure}`. `assert_valid_keys` raises rather than
1566
1566
  # mutating (raising is not an effect label).
1567
1567
  %a{pure}
1568
- def symbolize_keys: () -> Hash[Symbol, V]
1568
+ # `key.to_sym rescue key` keeps a key without `to_sym`, so no one key type is right: `Hash[untyped, V]` here,
1569
+ # and the plugin's `dynamic_return` hook (#2433) maps each key member where it can.
1570
+ def symbolize_keys: () -> Hash[untyped, V]
1571
+ # `symbolize_keys!` keeps `-> self`: `key.to_sym rescue key` leaves a key without `to_sym` as it is, so
1572
+ # `Hash[Symbol, V]` is wrong for an Integer-keyed receiver (#2424 review). The other bang forms below
1573
+ # answer the re-keyed or re-valued hash.
1569
1574
  def symbolize_keys!: () -> self
1570
1575
  %a{pure}
1571
1576
  def deep_symbolize_keys: () -> Hash[untyped, untyped]
1572
- def deep_symbolize_keys!: () -> self
1577
+ def deep_symbolize_keys!: () -> Hash[untyped, untyped]
1573
1578
  %a{pure}
1574
1579
  def stringify_keys: () -> Hash[String, V]
1575
- def stringify_keys!: () -> self
1580
+ def stringify_keys!: () -> Hash[String, V]
1576
1581
  %a{pure}
1577
1582
  def deep_stringify_keys: () -> Hash[untyped, untyped]
1578
- def deep_stringify_keys!: () -> self
1583
+ def deep_stringify_keys!: () -> Hash[untyped, untyped]
1579
1584
  %a{pure}
1580
1585
  def assert_valid_keys: (*K | Array[K]) -> self
1586
+ # The block's result is the new key at every level, so the keys are its type, not `K`: `{ a: 1 }
1587
+ # .deep_transform_keys(&:to_s)` has String keys. Nested values are re-keyed too, so they read `untyped`.
1581
1588
  %a{pure}
1582
- def deep_transform_keys: () { (K) -> K } -> Hash[K, untyped]
1583
- def deep_transform_keys!: () { (K) -> K } -> self
1589
+ def deep_transform_keys: [X] () { (K) -> X } -> Hash[X, untyped]
1590
+ def deep_transform_keys!: [X] () { (K) -> X } -> Hash[X, untyped]
1584
1591
  %a{pure}
1585
1592
  def deep_transform_values: () { (V) -> untyped } -> Hash[K, untyped]
1586
- def deep_transform_values!: () { (V) -> untyped } -> self
1593
+ def deep_transform_values!: () { (V) -> untyped } -> Hash[K, untyped]
1587
1594
  # `alias_method`s of `symbolize_keys` / `symbolize_keys!`.
1588
1595
  %a{pure}
1589
- def to_options: () -> Hash[Symbol, V]
1596
+ def to_options: () -> Hash[untyped, V]
1590
1597
  def to_options!: () -> self
1591
1598
 
1592
1599
  # `core_ext/hash/deep_dup` — allocates a deep copy, never touches the
@@ -1600,11 +1607,17 @@ class Hash[unchecked out K, unchecked out V]
1600
1607
  # `core_ext/hash/deep_merge` — the block receives `(key, this_val,
1601
1608
  # other_val)` and returns the merged value; neither `deep_merge` nor its
1602
1609
  # block mutates either hash (containment covers the block itself).
1603
- %a{pure}
1604
- def deep_merge: (Hash[K, V]) -> Hash[K, V]
1605
- | (Hash[K, V]) { (K, V, V) -> V } -> Hash[K, V]
1606
- def deep_merge!: (Hash[K, V]) -> self
1607
- | (Hash[K, V]) { (K, V, V) -> V } -> self
1610
+ # The result carries the argument's keys and values as well as the
1611
+ # receiver's, and a key both hold a Hash under merges into a third Hash
1612
+ # that is neither side's value type (`{ n: { x: 1 } }.deep_merge(n: { y:
1613
+ # "s" })[:n]` is `{ x: 1, y: "s" }`), so the values read `untyped`.
1614
+ # `(Hash[K, V]) -> Hash[K, V]` typed `h.deep_merge(b: "x")[:b]` as the
1615
+ # receiver's value type (#2424 review). The bang form answers the receiver.
1616
+ %a{pure}
1617
+ def deep_merge: [A, B] (Hash[A, B] other_hash) -> Hash[K | A, untyped]
1618
+ | [A, B] (Hash[A, B] other_hash) { (K | A, untyped, untyped) -> untyped } -> Hash[K | A, untyped]
1619
+ def deep_merge!: [A, B] (Hash[A, B] other_hash) -> Hash[K | A, untyped]
1620
+ | [A, B] (Hash[A, B] other_hash) { (K | A, untyped, untyped) -> untyped } -> Hash[K | A, untyped]
1608
1621
  # `other.is_a?(Hash)` — the hook `ActiveSupport::DeepMergeable` asks before
1609
1622
  # recursing into a value. `:nodoc:`, but public.
1610
1623
  %a{pure}
@@ -37,9 +37,9 @@ module Rigor
37
37
  @classes.empty?
38
38
  end
39
39
 
40
- # The classes alba infers for the associations that name no resource, limited to
41
- # those that exist in the project. The block receives each association name and returns alba's
42
- # `classify` of it (the caller owns the inflector; ADR-39).
40
+ # The classes alba infers for the associations that name no resource, limited to those that exist in
41
+ # the project. The block receives each association name and returns alba's `classify` of it (the
42
+ # caller owns the inflector; ADR-39).
43
43
  def inferred_roots(&classify)
44
44
  roots = @associations.filter_map do |association|
45
45
  next unless resource?(association.owner)
@@ -33,9 +33,11 @@ module Rigor
33
33
  # - `:authenticatable` — always-included base module
34
34
  #
35
35
  # The substrate's pre-pass scanner consults each module's RBS via `Environment::RbsLoader#instance_definition`
36
- # to enumerate method names. A user project providing real Devise via Bundler will see methods like
37
- # `valid_password?`, `send_reset_password_instructions`, etc. resolve through the synthetic-method
38
- # tier without `call.undefined-method`.
36
+ # to enumerate method names, so it names only what the project's RBS declares. The `devise` gem ships no
37
+ # RBS and Rigor bundles none for `Devise::Models::*`: a project supplies those signatures (`signature_paths:`
38
+ # or an `rbs collection`), and only then do `valid_password?`, `send_reset_password_instructions`, etc.
39
+ # resolve through the synthetic-method tier. A name the model defines itself, or a project module it
40
+ # includes or prepends defines, is left to the project's definition.
39
41
  #
40
42
  # ## Precision tier
41
43
  #
@@ -212,9 +212,7 @@ module Rigor
212
212
  # ... end` row's own recursive call (see {#each_block_type_info}). It caps the recursion at ONE
213
213
  # level deep: with `nested: true`, {#walk_predicate_chain} declines a FURTHER `each do ... end`
214
214
  # rather than recursing again (issue #137 deliberately does not model doubly-nested arrays of
215
- # arrays). This also keeps `collect_schema_shape` from calling itself — {#each_block_type_info}
216
- # is the only caller that ever passes `nested: true}` — so the call graph has no cycle for the
217
- # engine's return-type inference to approximate through.
215
+ # arrays). The recursion through {#each_block_type_info} is therefore bounded and terminates.
218
216
  def collect_schema_shape(block_node, type_aliases, nested:)
219
217
  required = {}
220
218
  optional = {}
@@ -34,9 +34,9 @@ module Rigor
34
34
  #
35
35
  # The synthetic reader's return type is resolved via ADR-18's `returns_from_arg:` fact lookup: the
36
36
  # call-site's second argument (`Types::String` etc.) is looked up through the `:dry_type_aliases`
37
- # fact published by `rigor-dry-types`, yielding `Nominal[String]` for common cases. When the lookup
38
- # misses (e.g. inline method-chain argument whose chain-head isn't currently extracted), the row
39
- # falls back to `Dynamic[Top]` silently.
37
+ # fact published by `rigor-dry-types`, yielding the aliased class (`String`) for common cases. When the
38
+ # lookup misses (e.g. inline method-chain argument whose chain-head isn't currently extracted), the row
39
+ # falls back to the untyped floor silently.
40
40
  #
41
41
  # ## Scope (slice 2c minimum)
42
42
  #
@@ -58,8 +58,8 @@ module Rigor
58
58
  "by consuming `rigor-dry-types`'s `:dry_type_aliases` fact.",
59
59
  # ADR-9 consumption — the precision-promotion path below uses `:dry_type_aliases` published by
60
60
  # `rigor-dry-types`. The fact is optional: when the `rigor-dry-types` plugin isn't loaded, the
61
- # `returns_from_arg:` lookup misses and the synthetic readers fall back to `Dynamic[Top]` (the
62
- # pre-ADR-18 floor).
61
+ # `returns_from_arg:` lookup misses and the synthetic readers fall back to the untyped floor (the
62
+ # pre-ADR-18 behaviour).
63
63
  consumes: [{ plugin_id: "dry-types", name: :dry_type_aliases, optional: true }],
64
64
  heredoc_templates: [
65
65
  Rigor::Plugin::Macro::HeredocTemplate.new(
@@ -69,7 +69,7 @@ module Rigor
69
69
  # ADR-18 — the synthetic reader's return type comes from the call site's second argument
70
70
  # (`Types::String` etc.), resolved through the `:dry_type_aliases` fact. When the lookup
71
71
  # misses (e.g. inline `attribute :tag, Types::String.constrained(...)`, whose receiver chain
72
- # head isn't currently extracted), the row falls back to Dynamic[Top].
72
+ # head isn't currently extracted), the row falls back to the untyped floor.
73
73
  emit: [{
74
74
  name: "\#{name}",
75
75
  returns_from_arg: {