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
@@ -45,8 +45,7 @@ module Rigor
45
45
  unique_constraint foreign_key primary_keys
46
46
  ].freeze
47
47
 
48
- # @param source [String] contents of `db/schema.rb`
49
- # @return [SchemaTable]
48
+ # @param source — contents of `db/schema.rb`
50
49
  def self.parse(source)
51
50
  tree = Prism.parse(source).value
52
51
  new.parse(tree)
@@ -54,8 +54,7 @@ module Rigor
54
54
  # Uppercase leading keywords that mark a table-level constraint line rather than a column.
55
55
  CONSTRAINT_KEYWORDS = %w[CONSTRAINT PRIMARY UNIQUE CHECK FOREIGN EXCLUDE LIKE PARTITION].freeze
56
56
 
57
- # @param source [String] contents of `db/structure.sql`
58
- # @return [SchemaTable]
57
+ # @param source — contents of `db/structure.sql`
59
58
  def self.parse(source)
60
59
  new.parse(source)
61
60
  end
@@ -63,6 +63,37 @@ module Rigor
63
63
  class Activerecord < Rigor::Plugin::Base
64
64
  manifest(
65
65
  id: "activerecord",
66
+ target_gems: ["activerecord"],
67
+ # 0.13.0, 2026-09-24 (#1321) — the class-side `Model.find` with two or more arguments returns
68
+ # `Array[Model]` instead of the model, and its `model-call` note says so. A model that defines its own
69
+ # `self.find` keeps its own answer; the note reads syntax alone and does not see that override. No
70
+ # producer payload changed shape; the bump records the type change as the 0.11.0 and 0.12.0 entries do.
71
+ #
72
+ # 0.12.0, 2026-09-19 (#1089) — a written-receiver read of an enum-backed column returns the enum's
73
+ # KEY type (the key String constants, or `String` for an empty key list) instead of the column's
74
+ # SQL storage type. No producer payload changed shape, but the version is the cache key a project
75
+ # sees and this changes which calls the plugin claims at the written-receiver spelling — a warm
76
+ # run must not serve the pre-change stream (the same call the 0.11.0 bump records).
77
+ #
78
+ # 0.11.0, 2026-09-17 (#963 item 2) — an implicit-self read of a column / association / `column?`
79
+ # predicate inside the model's own `def` is answered `untyped` instead of reaching no path, so the
80
+ # engine's own-method veto sees the member and a same-named top-level `def` stops binding ahead of
81
+ # it. No producer payload changed shape, but the version is the cache key a project sees and this
82
+ # changes which calls the plugin claims — a warm run must not serve the pre-change stream.
83
+ #
84
+ # 0.10.0, 2026-09-17 (#1049) — three macro families join the model entry and the published
85
+ # `:model_index` fact: `delegate`, associations declared in an included concern's `included do`, and
86
+ # the Paperclip / Active Storage attachment macros. `ModelIndex::Entry` gains a `macro_methods`
87
+ # member, so a cached 0.9.0 payload is not Marshal-compatible; and the concern fold WIDENS the
88
+ # association list of models the 0.9.0 payload recorded without it. Both make the bump — part of the
89
+ # producer cache KEY — what stops a warm run serving the pre-change surface to a consumer that fails
90
+ # closed on a name it cannot see.
91
+ #
92
+ # 0.9.0, 2026-09-10 (#534 item 7) — the bundled signatures gained `sig/active_record/framework.rbs`:
93
+ # the Active Record exception hierarchy, the `ActiveModel` namespace and `Arel`. No producer payload
94
+ # changed shape, but the version is the cache key a project sees, and the RBS half of a plugin's
95
+ # contribution rides the same bundle.
96
+ #
66
97
  # 0.8.0, 2026-09-02 — a model declared `class ::User` is keyed by its de-rooted name (`"User"`) in
67
98
  # the {ModelIndex} and the published `:model_index` fact (#583). The version is part of the
68
99
  # producer cache KEY: a cached 0.7.0 index still keys such a model `"::User"`, and `ModelIndex#find`
@@ -87,7 +118,7 @@ module Rigor
87
118
  # a scope lambda body / class-method body now contributes `Relation[Model]` via `scope.self_type`
88
119
  # instead of falling through to `Kernel#select` (the IO multiplexer, `Array[String]` return). Plus
89
120
  # `:select` added to the relation-entry-point list.
90
- version: "0.8.0",
121
+ version: "0.13.0",
91
122
  description: "Types ActiveRecord finders against the project's db/schema.rb and AR models.",
92
123
  config_schema: {
93
124
  "schema_file" => { kind: :string, default: "db/schema.rb" },
@@ -104,7 +135,28 @@ module Rigor
104
135
  # ADR-26 — `ActiveRecord::Relation` is an "open" receiver: it delegates an unbounded set of
105
136
  # user-defined scopes / class methods to its model, so `call.undefined-method` must not fire for
106
137
  # it. `CheckRules` reads this manifest field and skips the rule for the class.
107
- open_receivers: ["ActiveRecord::Relation"],
138
+ #
139
+ # The `sig/active_record/framework.rbs` names (#534 item 7) are open for the narrower reason that
140
+ # file documents: each is declared so the CONSTANT resolves, and none of them enumerates a method
141
+ # surface. `RecordInvalid#record`, `StatementInvalid#sql`, `Arel.star` and every other member left
142
+ # out must stay lenient, so the declaration buys constant resolution and asserts nothing else.
143
+ open_receivers: [
144
+ "ActiveRecord::Relation",
145
+ "ActiveRecord::ActiveRecordError",
146
+ "ActiveRecord::RecordNotFound",
147
+ "ActiveRecord::RecordInvalid",
148
+ "ActiveRecord::RecordNotSaved",
149
+ "ActiveRecord::RecordNotDestroyed",
150
+ "ActiveRecord::ReadOnlyRecord",
151
+ "ActiveRecord::Rollback",
152
+ "ActiveRecord::StaleObjectError",
153
+ "ActiveRecord::ConnectionNotEstablished",
154
+ "ActiveRecord::StatementInvalid",
155
+ "ActiveRecord::WrappedDatabaseException",
156
+ "ActiveRecord::RecordNotUnique",
157
+ "ActiveModel::ValidationError",
158
+ "Arel"
159
+ ],
108
160
  # ADR-103 WD2 / WD10 (#387) — the effect layer. rigor-activerecord models ActiveRecord, which is
109
161
  # part of Rails, so it opens the framework's own `rails.*` root rather than one named after the
110
162
  # plugin; `effect_root:` is granted only because the engine bundles this plugin
@@ -184,6 +236,10 @@ module Rigor
184
236
  # no-op).
185
237
  def prepare(services)
186
238
  index = model_index
239
+ # Issue #1051 — registered HERE, on the run's parent, because `#prepare` is where the schema read is
240
+ # forced and therefore where every load error already exists. The pool's one pre-fork session runs
241
+ # this, so the disclosure is in the coordinator's table before a single worker is forked.
242
+ disclose_load_errors
187
243
  return if index.nil? || index.empty?
188
244
  # Reduced mode stays UNPUBLISHED. Every consumer reads `columns:` as authoritative and fires on a
189
245
  # key missing from it — rigor-actionpack's `permit(:title)` check and rigor-shoulda-matchers'
@@ -200,15 +256,17 @@ module Rigor
200
256
  )
201
257
  end
202
258
 
203
- def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
259
+ def diagnostics_for_file(path:, scope:, root:)
204
260
  index = model_index
205
261
  # The schema-load disclosure is independent of whether an index was built: reduced mode still
206
262
  # produces one (and still analyzes), while a genuine index failure produces one and nothing else.
207
- diagnostics = consume_load_error_diagnostics(path)
208
- return diagnostics if index.nil? || index.empty?
209
- return diagnostics if migration_path?(path)
263
+ # It leaves through the run-scoped channel rather than this method's return, so re-registering here
264
+ # is free — it only matters for a load error appended after `#prepare` already ran.
265
+ disclose_load_errors
266
+ return [] if index.nil? || index.empty?
267
+ return [] if migration_path?(path)
210
268
 
211
- diagnostics.concat(Analyzer.new(path: path, model_index: index).analyze(root).diagnostics)
269
+ Analyzer.new(path: path, model_index: index, scope: scope).analyze(root).diagnostics
212
270
  end
213
271
 
214
272
  # Rails migration files (`db/migrate/<timestamp>_*.rb`) and post-migration files
@@ -229,6 +287,61 @@ module Rigor
229
287
  MIGRATION_PATH_PATTERNS.any? { |pattern| path_s.match?(pattern) }
230
288
  end
231
289
 
290
+ # ADR-113 WD4 (#1082) — enumerates the members this plugin synthesizes on `class_name`, for the
291
+ # `rigor lens` declaration map. The rows come straight from the prepared {ModelIndex} — the same
292
+ # fact the `dynamic_return` gate and {#diagnostics_for_file} already read — so the hook adds no
293
+ # discovery of its own and stays off the `check` hot path (ADR-52): `check` never calls it. An
294
+ # unknown class, a failed index, and a non-model class all answer `[]`; a reduced-mode index (no
295
+ # schema) simply lists no columns.
296
+ #
297
+ # `type` is the most conservative of the answers the plugin gives the member at `check` — a row
298
+ # that claimed a type `check` never produces would misstate the member (ADR-113 treats a lens
299
+ # answer differing from `check` as a bug):
300
+ #
301
+ # - `:scope` — `Relation[Model]`, the class-side answer `Post.published` gets.
302
+ # - `:association_reader` — the same object {#association_return_type} contributes at a call
303
+ # site: `Relation[Target]` for a collection, `Nominal[Target]` (`| nil` for `has_one` /
304
+ # `optional: true`) for a singular, and nil for a polymorphic target the plugin cannot type.
305
+ # - `:column_reader` / `:column_predicate` — `Dynamic[top]` ON PURPOSE (#963): that is the
306
+ # answer the bare, receiver-less read gets inside the model's own `def`, the only answer free
307
+ # of receiver-type context. A written `user.name` still narrows to the column's type, but the
308
+ # member-level answer stays dynamic — the precise variant at member level was measured at 57
309
+ # false positives on mastodon.
310
+ # - `:enum` — the enum's attribute reader (`status`), listed ONLY when no column row already
311
+ # carries the name (a non-column enum attribute, or a reduced-mode index with no schema). An
312
+ # enum-backed column appears under `:column_reader` instead, so no member lists twice. The
313
+ # plugin answers nothing for the bare enum attribute at `check`, so `type` is nil — not
314
+ # `untyped`, which would claim an answer `check` does not give. The per-value predicates Rails
315
+ # generates ride in `:macro_method` with their affix-resolved names.
316
+ # - `:macro_method` — `delegate` names, attachment readers, enum predicates (#1049 records names
317
+ # only): the plugin knows the member exists and answers no type for it, so `type` is nil.
318
+ def declared_members(class_name)
319
+ index = model_index
320
+ return [] if index.nil?
321
+
322
+ entry = index.find(class_name)
323
+ return [] if entry.nil?
324
+
325
+ untyped = Rigor::Type::Combinator.untyped
326
+ entry.column_names.flat_map do |name|
327
+ [{ name: name, kind: :column_reader, type: untyped },
328
+ { name: "#{name}?", kind: :column_predicate, type: untyped }]
329
+ end +
330
+ entry.associations.map do |association|
331
+ { name: association[:name], kind: :association_reader,
332
+ type: association_return_type(entry, association[:name]) }
333
+ end +
334
+ entry.scopes.map do |name|
335
+ { name: name, kind: :scope, type: relation_of(entry.class_name) }
336
+ end +
337
+ (entry.enums.keys - entry.column_names).map do |column|
338
+ { name: column, kind: :enum, type: nil }
339
+ end +
340
+ entry.macro_methods.map do |name|
341
+ { name: name, kind: :macro_method, type: nil }
342
+ end
343
+ end
344
+
232
345
  # The class-side finder / relation entry-point names `finder_return_type` recognises. Static half of
233
346
  # the `dynamic_return` name gate; the run-time half comes from the model index (scopes,
234
347
  # associations, columns).
@@ -287,15 +400,16 @@ module Rigor
287
400
  return nil if index.nil? || index.empty?
288
401
 
289
402
  if call_node.receiver
290
- class_call_return_type(call_node, index) ||
403
+ class_call_return_type(call_node, scope, index) ||
291
404
  relation_call_return_type(call_node, scope, index) ||
292
405
  instance_call_return_type(call_node, scope, index)
293
406
  else
294
- implicit_self_class_call_return_type(call_node, scope, index)
407
+ implicit_self_class_call_return_type(call_node, scope, index) ||
408
+ implicit_self_instance_member_type(call_node, scope, index)
295
409
  end
296
410
  end
297
411
 
298
- def class_call_return_type(call_node, index)
412
+ def class_call_return_type(call_node, scope, index)
299
413
  model_name = constant_receiver_name(call_node.receiver)
300
414
  return nil if model_name.nil?
301
415
 
@@ -303,7 +417,7 @@ module Rigor
303
417
  entry = index.find(model_name)
304
418
  return nil if entry.nil?
305
419
 
306
- finder_return_type(call_node, entry) ||
420
+ finder_return_type(call_node, entry, scope) ||
307
421
  class_scope_return_type(call_node, entry)
308
422
  end
309
423
 
@@ -329,20 +443,115 @@ module Rigor
329
443
  entry = index.find(self_type.class_name)
330
444
  return nil if entry.nil?
331
445
 
332
- finder_return_type(call_node, entry) ||
446
+ finder_return_type(call_node, entry, scope) ||
333
447
  class_scope_return_type(call_node, entry)
334
448
  end
335
449
 
336
- # Class-side finders + the class-side relation entry points. `find` / `find_by!` return the model;
337
- # `find_by` adds the `nil` arm; `where` / `all` / `order` / `limit` / `none` open a relation. The
338
- # relation then carries its element type through any further chained query method via the bundled
339
- # `ActiveRecord::Relation` RBS.
340
- def finder_return_type(call_node, entry)
450
+ # Implicit-self INSTANCE member read — `name` / `account` written without a receiver inside the
451
+ # model's own `def`, where `self` is `Nominal[Model]`. Both existing instance paths key on a WRITTEN
452
+ # receiver, so this spelling reached no path at all; issue #963 item 2 is what that costs beyond
453
+ # precision. The engine's own-method veto (#618) asks the dispatcher whether a plugin answers the
454
+ # name before letting a same-named top-level `def` bind, and a member no tier answers is a member the
455
+ # top-level `def` binds ahead of — typing `name.upcase` inside `def shout` as the def's `nil` and
456
+ # firing `undefined method 'upcase' for nil` on working Rails code.
457
+ #
458
+ # **`untyped`, not the column's type**, and the difference is measured rather than assumed. The
459
+ # precise variant (the same `association_return_type` / `column_return_type` pair the written-receiver
460
+ # path uses) was run over the corpus and added 57 diagnostics on mastodon and 5 on redmine, all new:
461
+ # `flow.always-truthy-condition` where a predicate reader now folds a guard to a constant, and
462
+ # `call.possible-nil-receiver` where a singular association's `nil` arm survives a `.compact` the
463
+ # engine does not fold. Those are precision questions about the reader's TYPE, and each is a false
464
+ # positive on correct code. What #963 needs is only that the member EXISTS, so the contribution is the
465
+ # ADR-82 WD4 answer `#ruby_type_to_type`'s decline already documents next door: the plugin knows the
466
+ # reader is there and declines to say more here. The site keeps the `Dynamic` it already had, gains
467
+ # the `framework_dsl_boundary` attribution, and the veto gets its answer.
468
+ #
469
+ # The project's OWN definition wins, and is not merely a precision preference: `def name` on the
470
+ # model — or on an ancestor it declares — is an override Ruby dispatches to, and the plugin tier sits
471
+ # ABOVE the engine's body-inference tiers, so answering here would displace the override's real return
472
+ # type with `Dynamic`. `discovered_method_through_ancestors?` is the same table the veto's own
473
+ # discovery arm reads, and it fails toward declining (a budget-exhausted walk answers true).
474
+ def implicit_self_instance_member_type(call_node, scope, index)
475
+ return nil if scope.nil?
476
+ return nil unless call_node.arguments.nil?
477
+ return nil unless call_node.block.nil?
478
+
479
+ self_type = scope.self_type
480
+ return nil unless self_type.is_a?(Rigor::Type::Nominal)
481
+
482
+ entry = index.find(self_type.class_name)
483
+ return nil if entry.nil?
484
+ return nil unless instance_member_name?(entry, call_node.name)
485
+ return nil if scope.discovered_method_through_ancestors?(self_type.class_name, call_node.name, :instance)
486
+ return nil if rbs_answers_before_object?(self_type.class_name, call_node.name, scope)
487
+
488
+ Rigor::Type::Combinator.untyped
489
+ end
490
+
491
+ # A signature the project wrote about the model — or about anything in the model's MRO — is
492
+ # authorship, and this tier sits above `RbsDispatch`, so answering `untyped` for a name RBS declares
493
+ # would displace the declared type at the implicit-self spelling ONLY, leaving `name` and `self.name`
494
+ # typed differently in one method body. Declining restores both: RBS answers the call, and the
495
+ # engine's own veto still sees the member through its pre-`::Object` RBS arm, so a top-level `def` of
496
+ # the name does not bind either.
497
+ #
498
+ # The cut-off is `Inference::ExternalAncestorResolution.declared_before_object?`'s and NOT its
499
+ # own-class sibling's, and the difference is the whole point: RBS's definition builder resolves
500
+ # through ancestors, so a reader declared on a `sig/application_record.rbs` superclass or on an
501
+ # `include`d module comes back with THAT owner, and an own-class test would call it undeclared and
502
+ # displace it one ancestor up. What the cut-off keeps out is an owner at or after a top-level
503
+ # `def`'s own rung — `Object`, `Kernel`,
504
+ # `BasicObject` — whose names say nothing about whether the model answers. Instance-side only, so a
505
+ # sidecar declaring `def self.name` is correctly not counted. Fail-soft: an unreadable environment
506
+ # declines to claim a declaration exists, which leaves the member visible to the veto.
507
+ TOP_LEVEL_DEF_OWNERS = %w[Object Kernel BasicObject].freeze
508
+ private_constant :TOP_LEVEL_DEF_OWNERS
509
+
510
+ def rbs_answers_before_object?(class_name, method_name, scope)
511
+ definition = services.reflection.instance_method_definition(class_name, method_name, scope: scope)
512
+ return false if definition.nil? || !definition.respond_to?(:defined_in)
513
+
514
+ defined_in = definition.defined_in
515
+ return false if defined_in.nil?
516
+
517
+ !TOP_LEVEL_DEF_OWNERS.include?(defined_in.to_s.delete_prefix("::"))
518
+ rescue StandardError
519
+ false
520
+ end
521
+
522
+ # Whether the model entry declares `method_name` as an instance-side member: an association accessor,
523
+ # a column reader, or the ActiveRecord-generated `column?` predicate. The same three families
524
+ # `#recognised_method_names` puts in the dispatch gate, asked of ONE entry.
525
+ def instance_member_name?(entry, method_name)
526
+ name = method_name.to_s
527
+ return true unless entry.association(method_name).nil?
528
+
529
+ column_name = name.end_with?("?") ? name[0..-2] : name
530
+ !entry.column(column_name).nil?
531
+ end
532
+
533
+ # Class-side finders + the class-side relation entry points. `find` returns the model, or an Array of
534
+ # 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` /
535
+ # `all` / `order` / `limit` / `none` open a relation. The relation then carries its element type
536
+ # through any further chained query method via the bundled `ActiveRecord::Relation` RBS.
537
+ def finder_return_type(call_node, entry, scope)
341
538
  case call_node.name
342
539
  when :find
343
- return nil if call_argument_count(call_node).zero?
540
+ return block_find_return_type(call_node, entry, scope) if call_node.block
344
541
 
345
- Rigor::Type::Combinator.nominal_of(entry.class_name)
542
+ arity = call_argument_count(call_node)
543
+ return nil if arity.zero?
544
+
545
+ # `Model.find` is `all.find`, so it answers by arity exactly as the bundled `Relation#find`
546
+ # overloads do, and the comment there says why one argument stays the model.
547
+ model = Rigor::Type::Combinator.nominal_of(entry.class_name)
548
+ return model if arity < 2
549
+
550
+ # A model that defines its own `self.find` (`find(owner, id)`) takes two arguments for its own
551
+ # reasons, so the Array reading of Rails' `find` is withheld and the project's method answers.
552
+ return nil if scope.discovered_method_through_ancestors?(entry.class_name, :find, :singleton)
553
+
554
+ Rigor::Type::Combinator.nominal_of("Array", type_args: [model])
346
555
  when :find_by!
347
556
  # The bang variant raises `RecordNotFound` instead of returning `nil`, so the result is
348
557
  # non-nullable.
@@ -369,6 +578,29 @@ module Rigor
369
578
  end
370
579
  end
371
580
 
581
+ # `Model.find { |m| … }` / `Model.find(&:pred?)` never reaches the id lookup: `Core.find` passes a
582
+ # block to `super` (`all.find`), whose `Relation#find` does `return super if block_given?`, landing on
583
+ # `Enumerable#find` over the loaded records. So the answer is the element or `nil`, exactly what the
584
+ # bundled `Relation#find` block overload says and what the engine already answers for `Array#find`.
585
+ # The `nil` arm is the search miss, not a worst-case reading: the block-form `find` sites in the
586
+ # survey corpus, all on Arrays, guard it (`&.`, `||=`, `while (x = …)`).
587
+ #
588
+ # Declining here is not neutral: when the model is RBS-known, a decline falls through to
589
+ # `singleton(Model)`, which declares no `find`, and fires `call.undefined-method` on working code.
590
+ # An argument next to the block is `Enumerable#find`'s `ifnone` callable, whose result joins the
591
+ # union. That form is rare enough that it answers `untyped` rather than typing the callable, as the
592
+ # `Relation#find` overload does. A project `self.find` keeps its own answer, as it does for the
593
+ # multi-id form.
594
+ def block_find_return_type(call_node, entry, scope)
595
+ return nil if scope.discovered_method_through_ancestors?(entry.class_name, :find, :singleton)
596
+ return Rigor::Type::Combinator.untyped unless call_argument_count(call_node).zero?
597
+
598
+ Rigor::Type::Combinator.union(
599
+ Rigor::Type::Combinator.nominal_of(entry.class_name),
600
+ Rigor::Type::Combinator.constant_of(nil)
601
+ )
602
+ end
603
+
372
604
  # `Model.table_name` is a String either way; whether it is safe to VALUE-PIN turns on how the name was
373
605
  # derived. `Entry#table_name_exact?` holds ONLY for a name the source declared as a String literal
374
606
  # (`self.table_name = "…"`, inherited down an STI chain, and not overridden by a computed one). An
@@ -511,16 +743,51 @@ module Rigor
511
743
  return nil if column.nil?
512
744
  return bool_type if predicate
513
745
 
746
+ # #1089 — an enum-backed column reads as its KEY (`"active"`), not its SQL storage type
747
+ # (`Integer` for a `t.integer :status` column). Rails returns the configured key as a String from
748
+ # the reader (`user.status # => "active"`), so typing it by `column.ruby_type` makes a correct
749
+ # `user.status.upcase` fire `call.undefined-method` on Integer and lets a wrong `user.status + 1`
750
+ # pass. The writer / `where(status:)` side is out of scope and untouched — it accepts both keys
751
+ # and values.
752
+ return enum_key_type(entry, column_name) if entry.enum?(column_name)
753
+
514
754
  inner = ruby_type_to_type(column.ruby_type)
515
- return nil if inner.nil?
755
+ return Rigor::Type::Combinator.untyped if inner.nil?
516
756
 
517
757
  column.array? ? Rigor::Type::Combinator.nominal_of("Array", type_args: [inner]) : inner
518
758
  end
519
759
 
520
- # Maps a `SchemaTable::Column#ruby_type` string to a Rigor type. `"Object"` (json / jsonb /
521
- # unrecognised column types) declines — `Nominal[Object]` would be NARROWER than the RBS-erased
522
- # envelope and could surface false `call.undefined-method` on a value whose real shape the plugin
523
- # cannot model.
760
+ # The KEY type of an enum-backed column. {ModelIndex} only records an enum when every key is a
761
+ # static Symbol literal — `parse_enum_call` declines a non-literal values expression outright — so
762
+ # a column in `entry.enums` carries a COMPLETE key set by construction, and the union of the key
763
+ # String constants is exact for the declared enum rather than the wider `String`. The empty-list
764
+ # guard (`enum :status, []` parses) falls back to `String`.
765
+ #
766
+ # Nullability follows `#column_return_type`'s other-column convention: non-nullable, so
767
+ # `user.status.upcase` does not light up `possible-nil-receiver` — Rails code calls enum readers
768
+ # directly as a matter of course, and the same under-report-the-nil trade the parent doc makes
769
+ # applies.
770
+ def enum_key_type(entry, column_name)
771
+ keys = entry.enum_values(column_name)
772
+ return Rigor::Type::Combinator.nominal_of("String") if keys.empty?
773
+
774
+ Rigor::Type::Combinator.union(
775
+ *keys.map { |key| Rigor::Type::Combinator.constant_of(key.to_s) }
776
+ )
777
+ end
778
+
779
+ # Maps a `SchemaTable::Column#ruby_type` string to a Rigor type. `"Object"` (json / jsonb / a
780
+ # `serialize`d or `mount_uploader`ed column / an unrecognised SQL type) declines — `Nominal[Object]`
781
+ # would be NARROWER than the RBS-erased envelope and could surface false `call.undefined-method` on a
782
+ # value whose real shape the plugin cannot model.
783
+ #
784
+ # ADR-82 WD4 — the caller turns that decline into an explicit `Dynamic[top]` rather than passing the
785
+ # call on. The two are the same type at the site (dispatch widens an unresolved reader to Dynamic
786
+ # anyway), but only the plugin ANSWERING makes the engine record `framework_dsl_boundary`: this is a
787
+ # macro-generated attribute whose value crosses a DSL boundary and stays dynamic, which is exactly the
788
+ # cause WD4 defined and left unattributed. Answering here is honest in the way WD4 requires — the
789
+ # plugin knows the reader exists (the schema declares the column) and knows it cannot type it — where
790
+ # guessing "framework boundary" for any unresolved receiver would not be.
524
791
  def ruby_type_to_type(ruby_type)
525
792
  case ruby_type
526
793
  when "bool" then bool_type
@@ -579,7 +846,8 @@ module Rigor
579
846
  scopes: entry.scopes,
580
847
  validations: entry.validated_attributes,
581
848
  callbacks: entry.callbacks,
582
- aliases: entry.aliases
849
+ aliases: entry.aliases,
850
+ macro_methods: entry.macro_methods
583
851
  }.freeze
584
852
  end.freeze
585
853
  end
@@ -597,7 +865,9 @@ module Rigor
597
865
  # model-file additions — so no priming walk is needed (it used to run the discover twice).
598
866
  @model_index = cache_for(:model_index, params: {}).call
599
867
  rescue StandardError => e
600
- @load_errors << { message: "model index build failed: #{e.class}: #{e.message}", severity: :warning }
868
+ @load_errors << { key: "2-model-index-failed",
869
+ message: "model index build failed: #{e.class}: #{e.message}",
870
+ severity: :warning }
601
871
  nil
602
872
  end
603
873
 
@@ -616,7 +886,8 @@ module Rigor
616
886
  # here.
617
887
  @schema_table = cache_for(:schema_table, params: {}).call
618
888
  rescue Plugin::AccessDeniedError => e
619
- @load_errors << { message: "rigor-activerecord: #{e.message}#{REDUCED_MODE_SUFFIX}",
889
+ @load_errors << { key: "1-schema-read-refused",
890
+ message: "rigor-activerecord: #{e.message}#{REDUCED_MODE_SUFFIX}",
620
891
  severity: :warning }
621
892
  nil
622
893
  rescue Errno::ENOENT
@@ -625,14 +896,16 @@ module Rigor
625
896
  # associations and table names off the discovered models, so this is a disclosure of REDUCED
626
897
  # capability rather than a failure — `:info`, the grade the plugins use for "here is what I
627
898
  # recognised", not `:warning`, the grade they use for "I could not run".
628
- @load_errors << { message: "rigor-activerecord: schema file `#{@schema_file}` (or " \
899
+ @load_errors << { key: "1-schema-file-missing",
900
+ message: "rigor-activerecord: schema file `#{@schema_file}` (or " \
629
901
  "`#{@structure_sql_file}`) not found#{REDUCED_MODE_SUFFIX}",
630
902
  severity: :info }
631
903
  nil
632
904
  rescue StandardError => e
633
905
  # A schema that EXISTS but does not parse is a real, actionable problem — the user meant it to be
634
906
  # read. Reduced mode still applies, but the disclosure stays a warning.
635
- @load_errors << { message: "rigor-activerecord: failed to parse `#{@schema_file}`: " \
907
+ @load_errors << { key: "1-schema-parse-failed",
908
+ message: "rigor-activerecord: failed to parse `#{@schema_file}`: " \
636
909
  "#{e.class}: #{e.message}#{REDUCED_MODE_SUFFIX}",
637
910
  severity: :warning }
638
911
  nil
@@ -644,23 +917,31 @@ module Rigor
644
917
  "(`where(col:)`, column readers) are skipped"
645
918
  private_constant :REDUCED_MODE_SUFFIX
646
919
 
647
- # Project-global disclosures, emitted once per run on the first analyzed file rather than once per
648
- # file. On a Redmine-shape project (migrations only, no `schema.rb`) the per-file form produced 346
649
- # identical rows; on a Solidus monorepo, 999.
650
- def consume_load_error_diagnostics(path)
651
- return [] if @load_errors_emitted
652
-
653
- @load_errors_emitted = true
654
- @load_errors.uniq.map do |error|
655
- Rigor::Analysis::Diagnostic.new(
656
- path: path,
657
- line: 1,
658
- column: 1,
920
+ # Project-global disclosures, handed to the engine's run-scoped channel ({Plugin::Base#disclose_once},
921
+ # issue #1051). They are facts about the project's INPUTS — "there is no `db/schema.rb`, so column
922
+ # checks are off" — so the engine positions them at `.rigor.yml:1:1` and emits each `key` once per
923
+ # run.
924
+ #
925
+ # This used to be an `@load_errors_emitted` flag consulted from `#diagnostics_for_file`, which put the
926
+ # row on whichever file this instance saw first. On a Redmine-shape project (migrations only, no
927
+ # `schema.rb`) the per-FILE form before that produced 346 identical rows; on a Solidus monorepo, 999.
928
+ # The flag fixed the count per instance, but a fork-pool worker gets its OWN instance, so `--workers
929
+ # N` re-multiplied it by N and each copy landed on that worker's first file — after #393 possibly an
930
+ # `.erb` template unit, where a schema notice reads as a claim about a view.
931
+ # The `key`s carry an ordinal prefix because the engine emits a plugin's disclosures in KEY order
932
+ # (#1051), and the three schema outcomes — mutually exclusive, since `@schema_load_attempted` memoises
933
+ # the attempt — must still precede a model-index failure the way `@load_errors` records them. Keys are
934
+ # identities, never shown to the user, so the prefix costs nothing but the ordering.
935
+ def disclose_load_errors
936
+ @load_errors.each do |error|
937
+ disclose_once(
938
+ error.fetch(:key),
659
939
  message: error.fetch(:message),
660
940
  severity: error.fetch(:severity),
661
941
  rule: "load-error"
662
942
  )
663
943
  end
944
+ nil
664
945
  end
665
946
  end
666
947
 
@@ -0,0 +1,123 @@
1
+ # Active Record's framework namespaces — issue #534 item 7.
2
+ #
3
+ # The 2026-09-01 corpus opacity sweep counted `ActiveRecord::RecordNotFound` 115 times on
4
+ # mastodon, modal in `rescue` clauses, and `ActiveModel` 165 / `Arel` 35 — every one of them
5
+ # resolving to `untyped` because nothing in the bundle named the constant. This file names them,
6
+ # and nothing more.
7
+ #
8
+ # ## The admission rule: a namespace or a rescued error, never a base class
9
+ #
10
+ # Declaring a constant makes it RBS-KNOWN, and a known class with an incomplete signature turns
11
+ # every member the signature omits into a false `call.undefined-method`. That risk is not
12
+ # uniform, so this file admits exactly two shapes:
13
+ #
14
+ # - **Bare namespace modules.** Nobody dispatches on a module used only as a constant path
15
+ # (`ActiveModel::Type::Boolean`, `Arel::Nodes`), so an empty declaration asserts nothing.
16
+ # - **Exception classes**, which are dispatched on — as the `=> e` of a rescue — but whose whole
17
+ # inherited surface is `StandardError`'s, which Rigor's core RBS already carries. Each is listed
18
+ # in the manifest's `open_receivers:` (ADR-26) as well, so the framework-specific readers this
19
+ # file does not enumerate (`RecordInvalid#record`, `StatementInvalid#cause`, …) stay lenient.
20
+ #
21
+ # What is deliberately NOT here is every class a project INHERITS from. `ActiveRecord::Base` is
22
+ # the case that matters: declaring it empty would close every model in the project, and turn
23
+ # `Account.where(...)`, `record.save!` and the whole generated column surface into
24
+ # `call.undefined-method` on working Rails code. The plugin types models through its model index
25
+ # instead, and the namespace stays open for it. The same argument keeps `ActiveModel::Model` and
26
+ # the other mixin modules bare rather than populated.
27
+ #
28
+ # The exception hierarchy is transcribed from Rails v8.0.2 `activerecord/lib/active_record/errors.rb`;
29
+ # a class whose superclass could not be stated with certainty was left out rather than guessed,
30
+ # because a wrong superclass is a wrong `rescue` reachability answer.
31
+
32
+ module ActiveRecord
33
+ class ActiveRecordError < StandardError
34
+ end
35
+
36
+ class RecordNotFound < ActiveRecordError
37
+ end
38
+
39
+ class RecordInvalid < ActiveRecordError
40
+ end
41
+
42
+ class RecordNotSaved < ActiveRecordError
43
+ end
44
+
45
+ class RecordNotDestroyed < ActiveRecordError
46
+ end
47
+
48
+ class ReadOnlyRecord < ActiveRecordError
49
+ end
50
+
51
+ class Rollback < ActiveRecordError
52
+ end
53
+
54
+ class StaleObjectError < ActiveRecordError
55
+ end
56
+
57
+ class ConnectionNotEstablished < ActiveRecordError
58
+ end
59
+
60
+ class StatementInvalid < ActiveRecordError
61
+ end
62
+
63
+ class WrappedDatabaseException < StatementInvalid
64
+ end
65
+
66
+ class RecordNotUnique < WrappedDatabaseException
67
+ end
68
+ end
69
+
70
+ # Active Model ships as its own gem, but `activerecord` depends on it unconditionally, so a project
71
+ # that enables this plugin always has it. There is no `rigor-activemodel`, and inventing one to hold
72
+ # eleven empty modules would be a plugin whose whole content is this file.
73
+ module ActiveModel
74
+ module Model
75
+ end
76
+
77
+ module API
78
+ end
79
+
80
+ module Validations
81
+ end
82
+
83
+ module Serialization
84
+ end
85
+
86
+ module Conversion
87
+ end
88
+
89
+ module Naming
90
+ end
91
+
92
+ module Attributes
93
+ end
94
+
95
+ module Dirty
96
+ end
97
+
98
+ module SecurePassword
99
+ end
100
+
101
+ module Callbacks
102
+ end
103
+
104
+ module Type
105
+ end
106
+
107
+ class ValidationError < StandardError
108
+ end
109
+ end
110
+
111
+ # Arel ships INSIDE the activerecord gem, so this plugin is its owner by the "the row lives in the
112
+ # plugin that owns the gem" rule. `Arel.sql` is the one member the corpus calls; the signature is
113
+ # written to accept anything because its extra binds are positional-or-keyword and an arity claim
114
+ # here would be a false `call.wrong-arity` on a shape this file has no reason to police.
115
+ module Arel
116
+ module Nodes
117
+ end
118
+
119
+ module Predications
120
+ end
121
+
122
+ def self.sql: (untyped, *untyped, **untyped) -> untyped
123
+ end