rigortype 0.3.9 → 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 (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -64,6 +64,31 @@ module Rigor
64
64
  manifest(
65
65
  id: "activerecord",
66
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
+ #
67
92
  # 0.9.0, 2026-09-10 (#534 item 7) — the bundled signatures gained `sig/active_record/framework.rbs`:
68
93
  # the Active Record exception hierarchy, the `ActiveModel` namespace and `Arel`. No producer payload
69
94
  # changed shape, but the version is the cache key a project sees, and the RBS half of a plugin's
@@ -93,7 +118,7 @@ module Rigor
93
118
  # a scope lambda body / class-method body now contributes `Relation[Model]` via `scope.self_type`
94
119
  # instead of falling through to `Kernel#select` (the IO multiplexer, `Array[String]` return). Plus
95
120
  # `:select` added to the relation-entry-point list.
96
- version: "0.9.0",
121
+ version: "0.13.0",
97
122
  description: "Types ActiveRecord finders against the project's db/schema.rb and AR models.",
98
123
  config_schema: {
99
124
  "schema_file" => { kind: :string, default: "db/schema.rb" },
@@ -211,6 +236,10 @@ module Rigor
211
236
  # no-op).
212
237
  def prepare(services)
213
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
214
243
  return if index.nil? || index.empty?
215
244
  # Reduced mode stays UNPUBLISHED. Every consumer reads `columns:` as authoritative and fires on a
216
245
  # key missing from it — rigor-actionpack's `permit(:title)` check and rigor-shoulda-matchers'
@@ -227,15 +256,17 @@ module Rigor
227
256
  )
228
257
  end
229
258
 
230
- def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
259
+ def diagnostics_for_file(path:, scope:, root:)
231
260
  index = model_index
232
261
  # The schema-load disclosure is independent of whether an index was built: reduced mode still
233
262
  # produces one (and still analyzes), while a genuine index failure produces one and nothing else.
234
- diagnostics = consume_load_error_diagnostics(path)
235
- return diagnostics if index.nil? || index.empty?
236
- 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)
237
268
 
238
- 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
239
270
  end
240
271
 
241
272
  # Rails migration files (`db/migrate/<timestamp>_*.rb`) and post-migration files
@@ -256,6 +287,61 @@ module Rigor
256
287
  MIGRATION_PATH_PATTERNS.any? { |pattern| path_s.match?(pattern) }
257
288
  end
258
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
+
259
345
  # The class-side finder / relation entry-point names `finder_return_type` recognises. Static half of
260
346
  # the `dynamic_return` name gate; the run-time half comes from the model index (scopes,
261
347
  # associations, columns).
@@ -314,15 +400,16 @@ module Rigor
314
400
  return nil if index.nil? || index.empty?
315
401
 
316
402
  if call_node.receiver
317
- class_call_return_type(call_node, index) ||
403
+ class_call_return_type(call_node, scope, index) ||
318
404
  relation_call_return_type(call_node, scope, index) ||
319
405
  instance_call_return_type(call_node, scope, index)
320
406
  else
321
- 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)
322
409
  end
323
410
  end
324
411
 
325
- def class_call_return_type(call_node, index)
412
+ def class_call_return_type(call_node, scope, index)
326
413
  model_name = constant_receiver_name(call_node.receiver)
327
414
  return nil if model_name.nil?
328
415
 
@@ -330,7 +417,7 @@ module Rigor
330
417
  entry = index.find(model_name)
331
418
  return nil if entry.nil?
332
419
 
333
- finder_return_type(call_node, entry) ||
420
+ finder_return_type(call_node, entry, scope) ||
334
421
  class_scope_return_type(call_node, entry)
335
422
  end
336
423
 
@@ -356,20 +443,115 @@ module Rigor
356
443
  entry = index.find(self_type.class_name)
357
444
  return nil if entry.nil?
358
445
 
359
- finder_return_type(call_node, entry) ||
446
+ finder_return_type(call_node, entry, scope) ||
360
447
  class_scope_return_type(call_node, entry)
361
448
  end
362
449
 
363
- # Class-side finders + the class-side relation entry points. `find` / `find_by!` return the model;
364
- # `find_by` adds the `nil` arm; `where` / `all` / `order` / `limit` / `none` open a relation. The
365
- # relation then carries its element type through any further chained query method via the bundled
366
- # `ActiveRecord::Relation` RBS.
367
- 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)
368
538
  case call_node.name
369
539
  when :find
370
- return nil if call_argument_count(call_node).zero?
540
+ return block_find_return_type(call_node, entry, scope) if call_node.block
371
541
 
372
- 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])
373
555
  when :find_by!
374
556
  # The bang variant raises `RecordNotFound` instead of returning `nil`, so the result is
375
557
  # non-nullable.
@@ -396,6 +578,29 @@ module Rigor
396
578
  end
397
579
  end
398
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
+
399
604
  # `Model.table_name` is a String either way; whether it is safe to VALUE-PIN turns on how the name was
400
605
  # derived. `Entry#table_name_exact?` holds ONLY for a name the source declared as a String literal
401
606
  # (`self.table_name = "…"`, inherited down an STI chain, and not overridden by a computed one). An
@@ -538,12 +743,39 @@ module Rigor
538
743
  return nil if column.nil?
539
744
  return bool_type if predicate
540
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
+
541
754
  inner = ruby_type_to_type(column.ruby_type)
542
755
  return Rigor::Type::Combinator.untyped if inner.nil?
543
756
 
544
757
  column.array? ? Rigor::Type::Combinator.nominal_of("Array", type_args: [inner]) : inner
545
758
  end
546
759
 
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
+
547
779
  # Maps a `SchemaTable::Column#ruby_type` string to a Rigor type. `"Object"` (json / jsonb / a
548
780
  # `serialize`d or `mount_uploader`ed column / an unrecognised SQL type) declines — `Nominal[Object]`
549
781
  # would be NARROWER than the RBS-erased envelope and could surface false `call.undefined-method` on a
@@ -614,7 +846,8 @@ module Rigor
614
846
  scopes: entry.scopes,
615
847
  validations: entry.validated_attributes,
616
848
  callbacks: entry.callbacks,
617
- aliases: entry.aliases
849
+ aliases: entry.aliases,
850
+ macro_methods: entry.macro_methods
618
851
  }.freeze
619
852
  end.freeze
620
853
  end
@@ -632,7 +865,9 @@ module Rigor
632
865
  # model-file additions — so no priming walk is needed (it used to run the discover twice).
633
866
  @model_index = cache_for(:model_index, params: {}).call
634
867
  rescue StandardError => e
635
- @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 }
636
871
  nil
637
872
  end
638
873
 
@@ -651,7 +886,8 @@ module Rigor
651
886
  # here.
652
887
  @schema_table = cache_for(:schema_table, params: {}).call
653
888
  rescue Plugin::AccessDeniedError => e
654
- @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}",
655
891
  severity: :warning }
656
892
  nil
657
893
  rescue Errno::ENOENT
@@ -660,14 +896,16 @@ module Rigor
660
896
  # associations and table names off the discovered models, so this is a disclosure of REDUCED
661
897
  # capability rather than a failure — `:info`, the grade the plugins use for "here is what I
662
898
  # recognised", not `:warning`, the grade they use for "I could not run".
663
- @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 " \
664
901
  "`#{@structure_sql_file}`) not found#{REDUCED_MODE_SUFFIX}",
665
902
  severity: :info }
666
903
  nil
667
904
  rescue StandardError => e
668
905
  # A schema that EXISTS but does not parse is a real, actionable problem — the user meant it to be
669
906
  # read. Reduced mode still applies, but the disclosure stays a warning.
670
- @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}`: " \
671
909
  "#{e.class}: #{e.message}#{REDUCED_MODE_SUFFIX}",
672
910
  severity: :warning }
673
911
  nil
@@ -679,23 +917,31 @@ module Rigor
679
917
  "(`where(col:)`, column readers) are skipped"
680
918
  private_constant :REDUCED_MODE_SUFFIX
681
919
 
682
- # Project-global disclosures, emitted once per run on the first analyzed file rather than once per
683
- # file. On a Redmine-shape project (migrations only, no `schema.rb`) the per-file form produced 346
684
- # identical rows; on a Solidus monorepo, 999.
685
- def consume_load_error_diagnostics(path)
686
- return [] if @load_errors_emitted
687
-
688
- @load_errors_emitted = true
689
- @load_errors.uniq.map do |error|
690
- Rigor::Analysis::Diagnostic.new(
691
- path: path,
692
- line: 1,
693
- 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),
694
939
  message: error.fetch(:message),
695
940
  severity: error.fetch(:severity),
696
941
  rule: "load-error"
697
942
  )
698
943
  end
944
+ nil
699
945
  end
700
946
  end
701
947