rigortype 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -38,32 +38,55 @@ module Rigor
38
38
 
39
39
  READ = ["io.db.read"].freeze
40
40
  WRITE = ["io.db.write"].freeze
41
+ READ_WRITE = ["io.db.read", "io.db.write"].freeze
41
42
  TRANSACTION = ["io.db.transaction"].freeze
42
43
  SCHEMA_WRITE = ["io.db.write", "rails.schema.write"].freeze
43
44
 
44
45
  # Class-side finders. Every one issues a `SELECT` the moment it is called — that is what separates
45
- # them from `where`, which returns a relation and issues nothing.
46
+ # them from `where`, which returns a relation and issues nothing. An `async_*` calculation schedules
47
+ # it on the async executor, or runs it at once when there is none. `first_or_initialize` is
48
+ # `first || new`, and `extract_associated` loads the association it preloads.
46
49
  SINGLETON_READS = %w[
47
50
  find find_by find_by! first first! last last! take take! sole find_sole_by
48
- second third fourth fifth forty_two second_to_last third_to_last
51
+ second second! third third! fourth fourth! fifth fifth! forty_two forty_two!
52
+ second_to_last second_to_last! third_to_last third_to_last!
49
53
  exists? any? none? one? many? empty? count sum average minimum maximum calculate
50
54
  pluck pick ids find_each find_in_batches in_batches find_by_sql count_by_sql
51
- find_or_initialize_by
55
+ find_or_initialize_by first_or_initialize extract_associated
56
+ async_ids async_count async_average async_minimum async_maximum async_sum async_pluck async_pick
52
57
  ].freeze
53
58
 
54
- # Class-side writers.
59
+ # Class-side writers that issue their statement and query nothing of their own: `create` is
60
+ # `new(…).save`, the bulk writers compile one `INSERT`, and the counter writers are an `update_all` on
61
+ # `unscoped`, which cannot eager-load.
55
62
  SINGLETON_WRITES = %w[
56
63
  create create! insert insert! insert_all insert_all! upsert upsert_all
57
- update update! update_all delete delete_all delete_by destroy destroy_all destroy_by
58
- find_or_create_by find_or_create_by! create_or_find_by create_or_find_by! touch_all
64
+ update_counters increment_counter decrement_counter
65
+ ].freeze
66
+
67
+ # Class-side writers that query as well, checked against activerecord 8.1.3.1. Most are in
68
+ # `Querying::QUERYING_METHODS`, which Rails delegates to `all`, so each is the Relation method of the
69
+ # same name and the row carries the read that `relation.rbs` gives it: a lookup before the write, a
70
+ # `find_by!` after a failed insert, the records `destroy_all` loads, or the `SELECT` of distinct
71
+ # primary keys an eager-loading, limited scope runs before its UPDATE / DELETE. `all` adds the default
72
+ # scope, which can eager-load. `delete` is `where(id:).delete_all`, and `destroy` calls `find`. Three
73
+ # are not delegated and read on their own: `update(!)` calls `find` or walks `all.each`, and
74
+ # `reset_counters` counts the association before it writes the column.
75
+ SINGLETON_READ_WRITES = %w[
76
+ update update! update_all delete delete_all delete_by destroy destroy_all destroy_by touch_all
77
+ find_or_create_by find_or_create_by! create_or_find_by create_or_find_by!
78
+ first_or_create first_or_create! reset_counters
59
79
  ].freeze
60
80
 
61
81
  # Instance-side readers. `reload` re-issues the `SELECT` and replaces the record's attributes,
62
82
  # which is a receiver mutation as well as a read.
63
83
  INSTANCE_READS = %w[valid? invalid? reload].freeze
64
84
 
65
- # Instance-side writers. Each is a statement issued now; `save`'s callbacks and validators are the
66
- # `effect_edges:` half, and arrive as edges rather than labels.
85
+ # Instance-side writers. Each is a statement issued now, and none queries on its own: `update` is
86
+ # `assign_attributes` plus `save`, and `increment!` / `update_columns` compile one UPDATE. What `save`
87
+ # or `destroy` reads beyond that comes from the model's callbacks and validators. The callback edge
88
+ # carries the ones it reads: symbol-argument callback macros and a uniqueness validator. Nothing
89
+ # carries the reads a required `belongs_to`, a `touch:` option or a `dependent:` option registers.
67
90
  INSTANCE_WRITES = %w[
68
91
  save save! update update! update_attribute update_attributes update_attributes!
69
92
  update_column update_columns destroy destroy! delete touch increment! decrement!
@@ -80,6 +103,26 @@ module Rigor
80
103
  all? any? none? one? include? member? first count sum
81
104
  ].freeze
82
105
 
106
+ # The writers an association's `CollectionProxy` defines, plus its `delete` / `destroy`, which
107
+ # override a plain Relation's own `delete(id_or_array)` / `destroy(id)`. Those are
108
+ # `where(id:).delete_all` and `find(id).destroy`, both inside this row's labels. Each adds records
109
+ # to the association's in-memory target or removes them from it. Depending on whether the owner is
110
+ # saved and on the association's `dependent:` option, it may also read, write, and open a
111
+ # transaction, so the row names all three rather than the parent `io.db`, which
112
+ # `--label io.db.write` would not match.
113
+ #
114
+ # The mutation is spelt from the call site, as every row here is, and bare `mutate` is the label
115
+ # for a receiver that is not the caller's `self`: the proxy is an object the caller holds, like the
116
+ # session in rigor-actionpack's `SESSION_WRITE`. The RBS envelopes on `build` / `reset` are written
117
+ # from the callee's side instead, and say `mutate.self`.
118
+ #
119
+ # A saved owner's `<<` saves the record, which runs the model's `before_save` / `after_commit`
120
+ # callbacks. No edge carries those to the caller, which is the same gap `Relation#create` has: the
121
+ # call site's receiver is `Relation[Post]`, and the edge that would reach `Post#save` needs the type
122
+ # argument, which the collector does not record (#1313).
123
+ PROXY_WRITERS = %w[<< push append concat replace delete destroy clear].freeze
124
+ PROXY_WRITE = ["io.db.read", "io.db.write", "io.db.transaction", "mutate"].freeze
125
+
83
126
  # Raw SQL, narrowed by the statement's own leading verb ({Rigor::Effects::Narrowing} `sql_verb`).
84
127
  ADAPTER_SQL = %w[execute exec_query exec_insert exec_update exec_delete select_all select_one
85
128
  select_value select_values select_rows query query_value query_values].freeze
@@ -103,6 +146,12 @@ module Rigor
103
146
  # Ambient AR calls that are neither a read nor a write of rows.
104
147
  TRANSACTIONAL = %w[transaction with_lock lock!].freeze
105
148
 
149
+ # The instance-side locks re-read the row: `lock!` is `reload(lock:)`, a `SELECT … FOR UPDATE`, and
150
+ # `with_lock` runs it inside a transaction. `lock!` opens no transaction of its own, so on it the
151
+ # transaction label names the one whose end releases the lock, and over-states a `lock!` outside one.
152
+ INSTANCE_LOCKS = %w[with_lock lock!].freeze
153
+ LOCK = ["io.db.read", "io.db.transaction"].freeze
154
+
106
155
  module_function
107
156
 
108
157
  def attributions
@@ -114,7 +163,11 @@ module Rigor
114
163
  why: "issues the SELECT at the call — this is the materializing " \
115
164
  "half of the builder/materializer split") +
116
165
  rows(BASE, SINGLETON_WRITES, WRITE, singleton: true,
117
- why: "issues an INSERT / UPDATE / DELETE at the call") +
166
+ why: "issues an INSERT at the call and queries nothing first") +
167
+ rows(BASE, SINGLETON_READ_WRITES, READ_WRITE,
168
+ singleton: true,
169
+ why: "issues an UPDATE / DELETE / INSERT at the call, and a SELECT before it or after " \
170
+ "a failed insert") +
118
171
  rows(BASE, TRANSACTIONAL, TRANSACTION, singleton: true,
119
172
  why: "opens a transaction; the block's own origins join " \
120
173
  "by containment, so the row states only the BEGIN")
@@ -125,14 +178,21 @@ module Rigor
125
178
  why: "re-reads the row (`reload`) or runs the validators, whose uniqueness checks query") +
126
179
  rows(BASE, INSTANCE_WRITES, WRITE,
127
180
  why: "persists the record — the write a `db: none` envelope is written to catch") +
128
- rows(BASE, TRANSACTIONAL, TRANSACTION,
129
- why: "opens a transaction / takes a row lock around the block")
181
+ rows(BASE, TRANSACTIONAL - INSTANCE_LOCKS, TRANSACTION, why: "opens a transaction around the block") +
182
+ rows(BASE, INSTANCE_LOCKS, LOCK,
183
+ why: "re-reads the row with `SELECT … FOR UPDATE`; `with_lock` does it inside a transaction")
130
184
  end
131
185
 
132
186
  def relation_rows
133
187
  rows(RELATION, RELATION_MATERIALIZERS, READ,
134
188
  why: "an Enumerable delegation on a Relation: it calls `each`, which runs the query. Not " \
135
- "declared in the bundled RBS because declaring it would change how it types")
189
+ "declared in the bundled RBS because declaring it would change how it types") +
190
+ rows(RELATION, PROXY_WRITERS, PROXY_WRITE,
191
+ why: "a CollectionProxy writer: it changes the association's target and, for a saved " \
192
+ "owner, the rows behind it. Keyed on Relation because that is the type the plugin " \
193
+ "gives an association reader. A plain Relation either has no such method or, through " \
194
+ "its own `delete` / `destroy`, deletes by id or finds and destroys, which stays inside " \
195
+ "the bound. The model's save callbacks are not edged")
136
196
  end
137
197
 
138
198
  # Two rows per selector, because raw SQL is written two ways and only one of them names a type.
@@ -77,9 +77,8 @@ module Rigor
77
77
  # `ApplicationRecord`) setting `self.table_name_prefix` for every model under it — both already
78
78
  # guess wrong identically on unpatched code.
79
79
  class ModelDiscoverer
80
- # @param io_boundary [Rigor::Plugin::IoBoundary]
81
- # @param search_paths [Array<String>] absolute or project-relative paths.
82
- # @param base_classes [Array<String>] superclass names that identify a class as an AR model.
80
+ # @param search_paths — absolute or project-relative paths.
81
+ # @param base_classes — superclass names that identify a class as an AR model.
83
82
  # Declaration macros whose column's runtime value is a rich object, not the SQL scalar. Their column
84
83
  # must NOT be narrowed to the schema type (see {ModelIndex.build}'s type-override remap).
85
84
  TYPE_OVERRIDE_METHODS = %i[serialize mount_uploader mount_uploaders].freeze
@@ -109,11 +108,23 @@ module Rigor
109
108
  # name — {#resolve_table_name_decorator} keeps walking outward past it, exactly like Rails' own
110
109
  # `respond_to?(:table_name_prefix)` ancestor search.
111
110
  @namespace_table_name_decorators = {}
111
+ # `concern module name => { scopes:, associations:, macro_methods: }` — what a module contributes
112
+ # to every class that includes it (#534 item 5, #1049). Unlike {@type_override_columns} these are
113
+ # NOT global: a declaration reaches a model only along a real `include` edge ({@include_edges}),
114
+ # so a same-named `scope` / association / `delegate` in a concern the model does not include
115
+ # contributes nothing.
116
+ @concern_declarations = {}
117
+ # `constant name => [raw included module name]` — every `include Foo` / `include Foo::Bar` a class
118
+ # or module body spells, as written. Resolution against the walked module set happens after the
119
+ # whole tree is read ({#resolve_concern_name}), because the including file may be walked first.
120
+ @include_edges = {}
121
+ # Every module declaration walked, for resolving an `include`'s relative constant spelling.
122
+ @module_names = Set.new
112
123
  end
113
124
 
114
125
  attr_reader :type_override_columns
115
126
 
116
- # @return [Array<Hash>] rows of { class_name:, table_name_override:, sti_parent:, ... }
127
+ # @return rows of { class_name:, table_name_override:, sti_parent:, ... }
117
128
  def discover
118
129
  candidates = []
119
130
  ruby_files_under(@search_paths).each do |path|
@@ -128,7 +139,8 @@ module Rigor
128
139
  # Every file is walked (and every module's `table_name_prefix` / `table_name_suffix` recorded)
129
140
  # before any row is resolved — a model's file may sort, and so be visited, before the file that
130
141
  # declares its enclosing module's decorator.
131
- attach_table_name_decorators(resolve_models(candidates), superclass_map: superclass_map)
142
+ rows = fold_concern_declarations(resolve_models(candidates), superclass_map: superclass_map)
143
+ attach_table_name_decorators(rows, superclass_map: superclass_map)
132
144
  end
133
145
 
134
146
  private
@@ -218,7 +230,8 @@ module Rigor
218
230
  scopes: (Array(base[:scopes]) + Array(addition[:scopes])).uniq,
219
231
  validations: (Array(base[:validations]) + Array(addition[:validations])).uniq,
220
232
  callbacks: (Array(base[:callbacks]) + Array(addition[:callbacks])).uniq,
221
- aliases: (base[:aliases] || {}).merge(addition[:aliases] || {})
233
+ aliases: (base[:aliases] || {}).merge(addition[:aliases] || {}),
234
+ macro_methods: (Array(base[:macro_methods]) + Array(addition[:macro_methods])).uniq
222
235
  )
223
236
  end
224
237
 
@@ -424,6 +437,7 @@ module Rigor
424
437
  superclass = strip_root(constant_path_name(node.superclass)) if node.superclass
425
438
 
426
439
  collect_type_overrides(node.body)
440
+ record_include_edges(full_name, node.body)
427
441
  # Rails' `full_table_name_prefix` walks `module_parents.detect { respond_to?(:table_name_prefix) }`
428
442
  # — lexical nesting, which does not care whether the namespace container was written `module Blog`
429
443
  # or `class Blog`. A `class` used purely as a namespace holder (or a model class that happens to
@@ -441,7 +455,8 @@ module Rigor
441
455
  scopes: lookup_scopes(node.body),
442
456
  validations: lookup_validations(node.body),
443
457
  callbacks: lookup_callbacks(node.body),
444
- aliases: lookup_aliases(node.body)
458
+ aliases: lookup_aliases(node.body),
459
+ macro_methods: lookup_macro_methods(node.body)
445
460
  })
446
461
 
447
462
  # Recurse into the body in case nested classes exist.
@@ -458,6 +473,9 @@ module Rigor
458
473
 
459
474
  full_name = declared_constant_name(module_local_name, lexical_path)
460
475
  record_table_name_decorators(full_name, node.body, is_class: false)
476
+ @module_names << full_name
477
+ record_concern_declarations(full_name, node.body)
478
+ record_include_edges(full_name, node.body)
461
479
 
462
480
  inner_path = [full_name]
463
481
  walk_for_classes(node.body, inner_path, &) if node.body
@@ -520,6 +538,177 @@ module Rigor
520
538
  type_arg.is_a?(Prism::ConstantReadNode) || type_arg.is_a?(Prism::ConstantPathNode)
521
539
  end
522
540
 
541
+ # Records what a concern module contributes to every class that includes it (#534 item 5, #1049;
542
+ # mastodon's `scope :without_suspended` in `Account::Suspensions`, `has_one :account_stat` and
543
+ # `delegate :statuses_count, …, to: :account_stat` in `Account::Counters`).
544
+ # ActiveSupport::Concern evaluates an `included do … end` block against the INCLUDING class, so
545
+ # whatever it declares is declared on every model that includes the module — and on no other model,
546
+ # which is why these are kept per-module here rather than globally the way {#collect_type_overrides}
547
+ # keeps column names.
548
+ #
549
+ # Concern-hood is recognised the way {#type_override_declaration_calls} already recognises it: by the
550
+ # `included do … end` block itself, not by an `extend ActiveSupport::Concern` line.
551
+ #
552
+ # The module's OWN top-level body contributes too, but only `delegate`: `delegate` is
553
+ # `Module#delegate`, so at a module's top level it defines ordinary INSTANCE methods on the module,
554
+ # which the including class then inherits through its ancestry — exactly the shape
555
+ # `Account::Counters` uses. A top-level `scope` / `has_one` / `has_attached_file` is by contrast a
556
+ # plain send to the module object (a runtime `NoMethodError`), never a class-level declaration on any
557
+ # model, so those are read from `included do … end` only.
558
+ def record_concern_declarations(module_name, body)
559
+ found = concern_declarations(body)
560
+ return if found.each_value.all?(&:empty?)
561
+
562
+ record = (@concern_declarations[module_name] ||= { scopes: [], associations: [], macro_methods: [] })
563
+ found.each { |key, values| record[key].concat(values) }
564
+ end
565
+
566
+ def concern_declarations(body)
567
+ record = { scopes: [], associations: [], macro_methods: [] }
568
+ return record if body.nil?
569
+
570
+ body.compact_child_nodes.each do |node|
571
+ next unless node.is_a?(Prism::CallNode) && node.receiver.nil?
572
+
573
+ if node.name == :included && node.block.is_a?(Prism::BlockNode)
574
+ block_body = node.block.body
575
+ record[:scopes].concat(lookup_scopes(block_body))
576
+ record[:associations].concat(lookup_associations(block_body))
577
+ record[:macro_methods].concat(lookup_macro_methods(block_body))
578
+ elsif node.name == :delegate
579
+ record[:macro_methods].concat(delegate_method_names(node))
580
+ end
581
+ end
582
+ record
583
+ end
584
+
585
+ # Records every `include Foo` / `include Foo::Bar` a class or module body spells, AS WRITTEN — the
586
+ # constant is resolved only once the whole tree has been walked ({#resolve_concern_name}), because the
587
+ # concern's file may well sort after the file that includes it. An `include` inside a concern's own
588
+ # `included do … end` block counts too: that block runs against the including class, so the module
589
+ # lands on the model exactly as a body-level `include` does. A non-constant argument
590
+ # (`include build_module`) renders as nil and declines.
591
+ def record_include_edges(constant_name, body)
592
+ names = include_call_targets(body)
593
+ return if names.empty?
594
+
595
+ (@include_edges[constant_name] ||= []).concat(names)
596
+ end
597
+
598
+ def include_call_targets(body)
599
+ return [] if body.nil?
600
+
601
+ body.compact_child_nodes.flat_map do |node|
602
+ next [] unless node.is_a?(Prism::CallNode) && node.receiver.nil?
603
+
604
+ if node.name == :included && node.block.is_a?(Prism::BlockNode)
605
+ include_call_targets(node.block.body)
606
+ elsif node.name == :include
607
+ Array(node.arguments&.arguments).filter_map { |arg| constant_path_name(arg) }
608
+ else
609
+ []
610
+ end
611
+ end
612
+ end
613
+
614
+ # Adds each model's concern-declared scopes, associations and macro methods to the ones its own body
615
+ # declares. Attribution is by real `include` edge: a concern nobody includes contributes nothing, and
616
+ # a same-named `scope` / `has_one` / `delegate` in an unrelated concern does not leak onto this model.
617
+ # A concern that includes another concern is followed transitively (Ruby's own semantics — the inner
618
+ # module ends up in the model's ancestry either way).
619
+ #
620
+ # The model's SUPERCLASS chain is walked too, because a concern is routinely included once in
621
+ # `ApplicationRecord` for every model to get. The chain is walked here rather than left to
622
+ # {ModelIndex.sti_chain}, which only links a model to another MODEL: a configured base class is not a
623
+ # discovered model, so `class Account < ApplicationRecord` has no `sti_parent` and the index's chain
624
+ # stops at `Account`. (A `scope` written directly in the base class's own body is still not
625
+ # propagated — that pre-existing gap is a different mechanism and is documented, not widened here.)
626
+ #
627
+ # The model's OWN association rows are kept LAST so {#dedup_named_rows}'s last-wins rule lets a class
628
+ # body redeclaration override the concern's, which is what Ruby does: the class's own
629
+ # `has_one :user, class_name: "…"` beats the one the included module installed.
630
+ def fold_concern_declarations(rows, superclass_map: {})
631
+ return rows if @concern_declarations.empty?
632
+
633
+ rows.map do |row|
634
+ inherited = ancestry_concern_declarations(row.fetch(:class_name), superclass_map)
635
+ next row if inherited.each_value.all?(&:empty?)
636
+
637
+ row.merge(
638
+ scopes: (Array(row[:scopes]) + inherited[:scopes]).uniq.freeze,
639
+ associations: dedup_named_rows(inherited[:associations] + Array(row[:associations])),
640
+ macro_methods: (Array(row[:macro_methods]) + inherited[:macro_methods]).uniq.freeze
641
+ )
642
+ end
643
+ end
644
+
645
+ # The concern declarations reaching `class_name` through its own includes and those of every
646
+ # superclass up the chain (`visited` stops a cycle a malformed source could spell).
647
+ def ancestry_concern_declarations(class_name, superclass_map)
648
+ found = { scopes: [], associations: [], macro_methods: [] }
649
+ curr = class_name
650
+ visited = Set.new
651
+
652
+ while curr && visited.add?(curr)
653
+ concern_declarations_for(curr).each { |key, values| found[key].concat(values) }
654
+ curr = superclass_map[curr]
655
+ end
656
+
657
+ found[:scopes].uniq!
658
+ found[:macro_methods].uniq!
659
+ found
660
+ end
661
+
662
+ # Breadth-first over the `include` edges reachable from `class_name`, unioning each visited concern's
663
+ # declarations. `visited` makes a mutually-including pair of concerns terminate rather than loop.
664
+ def concern_declarations_for(class_name)
665
+ found = { scopes: [], associations: [], macro_methods: [] }
666
+ queue = Array(@include_edges[class_name]).map { |raw| [raw, class_name] }
667
+ visited = Set.new([class_name])
668
+
669
+ until queue.empty?
670
+ raw, from_name = queue.shift
671
+ resolved = resolve_concern_name(raw, from_name)
672
+ next if visited.include?(resolved)
673
+
674
+ visited << resolved
675
+ (@concern_declarations[resolved] || {}).each { |key, values| found[key].concat(values) }
676
+ queue.concat(Array(@include_edges[resolved]).map { |inner| [inner, resolved] })
677
+ end
678
+
679
+ found
680
+ end
681
+
682
+ # Resolves an `include`'s constant spelling against the modules actually walked, mirroring Ruby's
683
+ # lexical lookup closely enough for the shapes concerns use: a rooted name (`include ::Suspendable`)
684
+ # names the top-level constant, a relative one is tried under the including constant and each of its
685
+ # enclosing namespaces first (`include Suspensions` inside `class Account` → `Account::Suspensions`,
686
+ # which is what Ruby resolves), and otherwise it is taken as written. A name no walked module answers
687
+ # to simply contributes nothing.
688
+ #
689
+ # Two known imprecisions, both cheap only to state:
690
+ #
691
+ # - The enclosing-namespace attempts are made for a COMPACT declaration too (`module A::B`'s own
692
+ # `include C` tries `A::B::C` then `A::C`), where Ruby's cref would only see `A::B` and the top
693
+ # level. Harmless: the extra candidates must match a module that was actually walked, and a
694
+ # project with both `A::B::C` and a top-level `C` concern would have to want the latter.
695
+ # - The final bare-name fall-back attributes BY SPELLING. A nearer constant declared outside
696
+ # `model_search_paths` — `Account::Suspensions` in `lib/`, say, while the concern roots hold a
697
+ # top-level `Suspensions` — is invisible to this walker, so the wrong one can win. Reading
698
+ # further than the model roots needs its own cache descriptor (see {#discover}'s `watch:` note),
699
+ # so the spelling is trusted instead.
700
+ def resolve_concern_name(raw, from_name)
701
+ name = strip_root(raw)
702
+ return name if raw.start_with?("::")
703
+
704
+ ([from_name] + namespace_ancestors(from_name)).each do |prefix|
705
+ candidate = "#{prefix}::#{name}"
706
+ return candidate if @module_names.include?(candidate)
707
+ end
708
+
709
+ name
710
+ end
711
+
523
712
  # Renders a constant-path node (`Admin::User`, `::ApplicationRecord`) as a String, keeping the
524
713
  # leading `::` of a rooted path — {#declared_constant_name} reads it as "reset the lexical path"
525
714
  # before the marker is dropped. Returns nil for shapes the discoverer chooses not to handle.
@@ -1334,6 +1523,193 @@ module Rigor
1334
1523
  aliases.freeze
1335
1524
  end
1336
1525
 
1526
+ # The attachment macros whose reader names are folded, mapped to the reader SUFFIXES each one
1527
+ # installs beside the bare accessor (#1049).
1528
+ #
1529
+ # Each key is the macro name of exactly one gem, and none of the three is a name another common
1530
+ # library claims — `has_attached_file` is Paperclip's, `has_one_attached` / `has_many_attached` are
1531
+ # Active Storage's — so the macro's presence in a model body IS the evidence that the project uses
1532
+ # that gem. No lockfile probe is needed (and none would be sound for a vendored or path-sourced
1533
+ # gem); a project that spells none of the three gets nothing folded.
1534
+ #
1535
+ # PAPERCLIP's set is deliberately minimal: `has_attached_file :file` installs `file`, `file=` and
1536
+ # `file?`, and those are the names #1049's serializer table needs. The `file_file_name` /
1537
+ # `file_content_type` / `file_file_size` / `file_updated_at` quartet is NOT listed because Paperclip
1538
+ # backs it with four real schema columns, which the index already carries from `db/schema.rb`;
1539
+ # synthesising them here would claim them on a model whose table lacks them.
1540
+ #
1541
+ # ACTIVE STORAGE's set is `avatar`, `avatar=`, `avatar_attachment`, `avatar_blob` for the singular
1542
+ # macro and `images`, `images=`, `images_attachments`, `images_blobs` for the collection one — the
1543
+ # `has_one_attached` / `has_many_attached` generated-methods surface, none of it column-backed.
1544
+ # This is name MEMBERSHIP only, and does not overlap `rigor-activestorage`, which contributes the
1545
+ # accessor's TYPE (`ActiveStorage::Attached::One`) and publishes no fact: a consumer that reads
1546
+ # `:model_index` to ask "does this model answer `avatar`?" gets nothing from that plugin, and gets
1547
+ # nothing at all when the project does not load it.
1548
+ ATTACHMENT_MACROS = {
1549
+ has_attached_file: ["", "=", "?"],
1550
+ has_one_attached: ["", "=", "_attachment", "_blob"],
1551
+ has_many_attached: ["", "=", "_attachments", "_blobs"]
1552
+ }.freeze
1553
+ Ractor.make_shareable(ATTACHMENT_MACROS)
1554
+ private_constant :ATTACHMENT_MACROS
1555
+
1556
+ # Instance-method names a declaration macro installs that neither the schema nor the association /
1557
+ # alias / scope lists already carry (#1049): `delegate`'s method names, the attachment macros'
1558
+ # readers, and an `enum`'s per-value predicates. Names ONLY — what a delegate target returns is a
1559
+ # separate question no consumer asks today.
1560
+ #
1561
+ # Every name here is spelled out by the declaration itself. A shape this walker cannot read off the
1562
+ # source (a non-literal `prefix:`, a `to:` that is not a Symbol when `prefix: true` needs its
1563
+ # spelling, a computed enum value list) contributes NOTHING rather than a guess: a consumer that
1564
+ # fails closed on an unanswered name loses precision when a name is missing, but is answered WRONG
1565
+ # when a name is invented.
1566
+ def lookup_macro_methods(body)
1567
+ return [] if body.nil?
1568
+
1569
+ names = []
1570
+ declaration_calls(body).each do |node|
1571
+ next if node.receiver
1572
+
1573
+ if node.name == :delegate
1574
+ names.concat(delegate_method_names(node))
1575
+ elsif ATTACHMENT_MACROS.key?(node.name)
1576
+ names.concat(attachment_method_names(node))
1577
+ elsif node.name == :enum
1578
+ names.concat(enum_predicate_names(node))
1579
+ end
1580
+ end
1581
+ names.uniq.freeze
1582
+ end
1583
+
1584
+ # `delegate :a, :b, to: :x` → `["a", "b"]`; `prefix: true` prepends the `to:` target's own spelling
1585
+ # (`delegate :can?, to: :user, prefix: true` → `"user_can?"`), and `prefix: :admin` prepends the
1586
+ # given one. ActiveSupport builds the name by plain concatenation, so a `?` / `!` / `=` suffix rides
1587
+ # along untouched.
1588
+ #
1589
+ # `allow_nil:` does not enter into it — it changes what the method RETURNS, never what it is called.
1590
+ # `prefix: true` with a `to:` this walker cannot render as a name declines the whole call.
1591
+ def delegate_method_names(node)
1592
+ args = node.arguments&.arguments
1593
+ return [] if args.nil? || args.empty?
1594
+
1595
+ names = args.filter_map { |arg| Rigor::Source::Literals.symbol_name(arg) }
1596
+ return [] if names.empty?
1597
+
1598
+ prefix = delegate_prefix(args)
1599
+ return [] if prefix == :unfoldable
1600
+
1601
+ names.map { |name| "#{prefix}#{name}" }
1602
+ end
1603
+
1604
+ # The `"user_"`-style prefix a `delegate` call's `prefix:` option establishes: `""` when the option
1605
+ # is absent or false, `:unfoldable` when it is present in a shape this walker cannot read.
1606
+ def delegate_prefix(args)
1607
+ value = keyword_option(args, "prefix")
1608
+ # `prefix: nil` is what ActiveSupport's own default is, and it reads as "no prefix" there, not as
1609
+ # an option the call spelled unreadably.
1610
+ return "" if value.nil? || value.is_a?(Prism::FalseNode) || value.is_a?(Prism::NilNode)
1611
+
1612
+ return delegate_target_prefix(args) if value.is_a?(Prism::TrueNode)
1613
+
1614
+ literal = name_literal(value)
1615
+ return :unfoldable if literal.nil? || literal.empty?
1616
+
1617
+ "#{literal}_"
1618
+ end
1619
+
1620
+ # `prefix: true` names the prefix after the `to:` target, so the target has to render as a name.
1621
+ # An instance-variable target (`to: :@user`, the spelling ActiveSupport accepts for delegating to an
1622
+ # ivar) does NOT: Rails strips the `@` when it builds the method name, and reading it as written
1623
+ # would invent `@user_email`.
1624
+ def delegate_target_prefix(args)
1625
+ name = name_literal(keyword_option(args, "to"))
1626
+ return :unfoldable if name.nil? || name.empty? || name.start_with?("@")
1627
+
1628
+ "#{name}_"
1629
+ end
1630
+
1631
+ # A Symbol or String literal rendered as a plain name, or nil for anything else. Rails accepts both
1632
+ # spellings wherever it takes a method or prefix name.
1633
+ def name_literal(node)
1634
+ Rigor::Source::Literals.symbol_name(node) || (node.is_a?(Prism::StringNode) ? node.unescaped : nil)
1635
+ end
1636
+
1637
+ # `has_attached_file :file` / `has_one_attached :avatar` / `has_many_attached :images` → the reader
1638
+ # names {ATTACHMENT_MACROS} lists for that macro. A non-Symbol-literal name declines.
1639
+ def attachment_method_names(node)
1640
+ name = Rigor::Source::Literals.symbol_name(node.arguments&.arguments&.first)
1641
+ return [] if name.nil? || name.empty?
1642
+
1643
+ ATTACHMENT_MACROS.fetch(node.name).map { |suffix| "#{name}#{suffix}" }
1644
+ end
1645
+
1646
+ # An `enum`'s per-value predicates — `enum :visibility, { … limited: 4 }, suffix: :visibility` →
1647
+ # `"limited_visibility?"` (mastodon's `Status::Visibility`). Rails composes the name as
1648
+ # `"#{prefix}#{value}#{suffix}"`, where `prefix: true` / `suffix: true` use the enum's own column
1649
+ # name and a Symbol / String uses that spelling; Rails 6's `_prefix:` / `_suffix:` keywords are read
1650
+ # as the same option.
1651
+ #
1652
+ # Only the `?` predicate is folded. The bang (`limited_visibility!`) writes to the database and the
1653
+ # same-named class-side scope is not an instance method at all; neither is a name a consumer of this
1654
+ # fact asks about, and both would be invention beyond what the table needs.
1655
+ #
1656
+ # The enum's COLUMN and value list are deliberately not folded into the row's `enums:` from here —
1657
+ # see {#concern_declarations}. This method contributes names, which every consumer of the fact
1658
+ # WIDENS a known-name set with; `enums:` drives `Analyzer#validate_enum_value`, which FIRES.
1659
+ #
1660
+ # `instance_methods: false` (Rails 7.1+) declines the whole call: that option is how a model asks
1661
+ # Rails NOT to define the per-value predicates, so folding them would claim methods the application
1662
+ # does not have — the one way this family could answer a fail-closed consumer wrongly rather than
1663
+ # just incompletely. A non-literal value for the option declines for the same reason.
1664
+ def enum_predicate_names(node)
1665
+ row = parse_enum_call(node)
1666
+ return [] if row.nil? || row[:values].empty?
1667
+
1668
+ args = node.arguments&.arguments || []
1669
+ instance_methods = keyword_option(args, "instance_methods")
1670
+ return [] unless instance_methods.nil? || instance_methods.is_a?(Prism::TrueNode)
1671
+
1672
+ prefix = enum_affix(args, %w[prefix _prefix], row[:column]) { |literal| "#{literal}_" }
1673
+ suffix = enum_affix(args, %w[suffix _suffix], row[:column]) { |literal| "_#{literal}" }
1674
+ return [] if prefix == :unfoldable || suffix == :unfoldable
1675
+
1676
+ row[:values].map { |value| "#{prefix}#{value}#{suffix}?" }
1677
+ end
1678
+
1679
+ # The rendered `prefix:` / `suffix:` affix of an `enum` call, `""` when the option is absent or
1680
+ # false and `:unfoldable` when it is present in a shape this walker cannot read.
1681
+ def enum_affix(args, keys, column_name)
1682
+ value = keys.filter_map { |key| keyword_option(args, key) }.first
1683
+ return "" if value.nil? || value.is_a?(Prism::FalseNode)
1684
+ return yield(column_name) if value.is_a?(Prism::TrueNode)
1685
+
1686
+ literal = name_literal(value)
1687
+ return :unfoldable if literal.nil? || literal.empty?
1688
+
1689
+ yield(literal)
1690
+ end
1691
+
1692
+ # The VALUE node of a keyword argument, across every keyword hash in the call's argument list, or
1693
+ # nil when the call does not spell that key. Unlike {#association_option} this hands back the node
1694
+ # rather than a Boolean, because the callers need Symbol and String values too.
1695
+ #
1696
+ # A BRACED hash argument (`enum :visibility, { limited: 4 }, suffix: :visibility`) is a
1697
+ # `Prism::HashNode` and is deliberately not scanned: there the braces hold the enum's own value map,
1698
+ # and an enum value spelled `prefix:` would otherwise read as the call's `prefix:` option. Rails 6's
1699
+ # `enum status: { … }, _prefix: true` puts both in ONE `KeywordHashNode`, which is scanned.
1700
+ def keyword_option(args, key)
1701
+ args.each do |arg|
1702
+ next unless arg.is_a?(Prism::KeywordHashNode)
1703
+
1704
+ arg.elements.each do |pair|
1705
+ next unless pair.is_a?(Prism::AssocNode) && Source::Literals.symbol_named?(pair.key, key)
1706
+
1707
+ return pair.value
1708
+ end
1709
+ end
1710
+ nil
1711
+ end
1712
+
1337
1713
  # Collects every Symbol-literal positional argument from a CallNode. Used by both
1338
1714
  # `lookup_validations` and `lookup_callbacks` to extract the attribute / method name list.
1339
1715
  def symbol_args(node)
@@ -15,7 +15,7 @@ module Rigor
15
15
  # Phase 2's schema is OPTIONAL. A project that ships raw migrations only — the DB-agnostic Rails
16
16
  # pattern, where `db/schema.rb` is gitignored (Redmine) — has no schema to combine, and `schema_table:
17
17
  # nil` then yields the REDUCED index: every {Entry} carries its resolved table name, associations,
18
- # enums, scopes, validations, callbacks and aliases, and an EMPTY column set. Only the column-keyed
18
+ # enums, scopes, validations, callbacks, aliases and macro methods, and an EMPTY column set. Only the column-keyed
19
19
  # surface stands down; the schema was never an input to any of the rest. {#columns_known?} tells the
20
20
  # two modes apart — an empty column set means "the schema is unknown", not "this model has no
21
21
  # columns", and consumers that would fire on an unrecognised column must not read the two the same
@@ -27,7 +27,7 @@ module Rigor
27
27
  #
28
28
  # Single-table-inheritance subclasses (`class Admin < User`) carry an `sti_parent:` pointer; their
29
29
  # {Entry} resolves its table from the root model and inherits the chain's declared associations /
30
- # enums / aliases / scopes / validations / callbacks.
30
+ # enums / aliases / scopes / validations / callbacks / macro methods.
31
31
  class ModelIndex
32
32
  # `associations` is a frozen `Array<Hash>` where each row carries `{ name:, kind:, target:,
33
33
  # nullable: }`:
@@ -47,7 +47,7 @@ module Rigor
47
47
  # exact. Only a true reading licenses pinning the name to a value; see
48
48
  # `Activerecord#table_name_return_type` for why the schema is not admitted as corroboration.
49
49
  Entry = Struct.new(:class_name, :table_name, :table_name_exact, :columns, :associations,
50
- :enums, :scopes, :validations, :callbacks, :aliases,
50
+ :enums, :scopes, :validations, :callbacks, :aliases, :macro_methods,
51
51
  keyword_init: true) do
52
52
  def table_name_exact? = table_name_exact == true
53
53
 
@@ -90,6 +90,13 @@ module Rigor
90
90
  # declarations.
91
91
  def alias?(name) = aliases.key?(name.to_s)
92
92
  def resolve_alias(name) = aliases[name.to_s]
93
+
94
+ # `macro_methods` is `Array<method_name>` — the instance methods a declaration macro installs that
95
+ # none of the sets above already carries (#1049): `delegate`'s names, the Paperclip / Active
96
+ # Storage attachment readers, and an `enum`'s per-value predicates. Names only, with no type: a
97
+ # consumer asks "does this model answer `followers_count`?", which is what a fail-closed check
98
+ # needs and all the declaration states.
99
+ def macro_method?(name) = macro_methods.include?(name.to_s)
93
100
  end
94
101
 
95
102
  attr_reader :entries
@@ -149,8 +156,8 @@ module Rigor
149
156
  end
150
157
 
151
158
  # STI children inherit their ancestors' declared associations / enums / aliases / scopes /
152
- # validations / callbacks. Without the merge a `where(<parent-association>: ...)` on the
153
- # child would surface as a false `unknown-column`.
159
+ # validations / callbacks / macro methods. Without the merge a `where(<parent-association>: ...)`
160
+ # on the child would surface as a false `unknown-column`.
154
161
  acc[class_name] = Entry.new(
155
162
  class_name: class_name,
156
163
  table_name: table_name,
@@ -161,7 +168,8 @@ module Rigor
161
168
  scopes: chain.flat_map { |r| Array(r[:scopes]) }.uniq.freeze,
162
169
  validations: chain.flat_map { |r| Array(r[:validations]) }.uniq.freeze,
163
170
  callbacks: chain.flat_map { |r| Array(r[:callbacks]) }.map(&:freeze).freeze,
164
- aliases: merge_aliases(chain)
171
+ aliases: merge_aliases(chain),
172
+ macro_methods: chain.flat_map { |r| Array(r[:macro_methods]) }.uniq.freeze
165
173
  ).freeze
166
174
  end
167
175
  new(entries.freeze, columns_known: !schema_table.nil?)