rigortype 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -5,6 +5,7 @@ require_relative "incremental"
5
5
  require_relative "incremental_run_slot"
6
6
  require_relative "incremental_run_slot/write_guard"
7
7
  require_relative "plugin_fact_fingerprint"
8
+ require_relative "run_disclosure_ledger"
8
9
  require_relative "source_rbs_gate"
9
10
  require_relative "../cache/file_digest"
10
11
  require_relative "../cache/incremental_snapshot"
@@ -26,10 +27,8 @@ module Rigor
26
27
  #
27
28
  # The invariant the verify harness (and the spec) assert: `#recheck`'s merged diagnostics are
28
29
  # byte-identical to a full `--no-cache` re-analysis of the edited tree, in the same order (#1524; the gate
29
- # itself compares sorted sets). This is the `--verify-incremental` acceptance gate, here without disk
30
- # persistence or CLI wiring (the cache is in-process). It models the body tier only: an edit that adds /
31
- # removes / moves a *file* is outside the analyzed set it maintains and falls to a fresh {#baseline} (the
32
- # structural tier is a later slice).
30
+ # itself compares sorted sets). The `--verify-incremental` gate runs this engine; `#run_incremental` adds the
31
+ # snapshot persistence that `--incremental` uses.
33
32
  # The class-length budget is relaxed: this is one cohesive orchestrator of the incremental state
34
33
  # (per-file diagnostics cache, the file-level / symbol-level / negative dependency graphs, and the ADR-85
35
34
  # seed bundles), clearer read together than split across micro-classes that would all share the same ivars.
@@ -100,8 +99,8 @@ module Rigor
100
99
  # ADR-85 WD2 — per-file discovery seed bundles keyed by logical path. A cold baseline builds them; a
101
100
  # warm recheck folds them (re-walking only changed files) and refreshes the set. Ride the snapshot.
102
101
  @seed_bundles = {}
103
- @cache = {} # analyzed path => [Diagnostic]
104
- @sources = {} # analyzed path => Set<source path it read from>
102
+ @cache = {} # analyzed path => its diagnostics
103
+ @sources = {} # analyzed path => the source paths it read from
105
104
  @digests = {} # analyzed path => ADR-87 packed stat-digest entry at last analysis
106
105
  # Issue #1583 — discovered-not-analysed path => the same packed entry, for a run over path arguments that
107
106
  # #684 widened to the configured `paths:`. Empty on every run over the configured paths.
@@ -109,18 +108,18 @@ module Rigor
109
108
  @analyzed = [] # the project files analyzed last round
110
109
  @dependents = {} # inverted @sources (file-level)
111
110
  # ADR-46 slice 4 — symbol-granularity tracking.
112
- @symbol_sources = {} # consumer => { source_path => Set<"ClassName#method"> }
113
- @ancestry_sources = {} # consumer => Set<source_path> (class-ancestry deps)
111
+ @symbol_sources = {} # consumer => { source_path => the "ClassName#method" symbols it read }
112
+ @ancestry_sources = {} # consumer => source paths (class-ancestry deps)
114
113
  @symbol_fingerprints = {} # path => { "ClassName#method" => sha256_hex }
115
- @symbol_dependents = {} # [source, symbol] => Set<consumer>
116
- @ancestry_dependents = {} # source => Set<consumer> (inverted ancestry_sources)
114
+ @symbol_dependents = {} # [source, symbol] => consumers
115
+ @ancestry_dependents = {} # source => consumers (inverted ancestry_sources)
117
116
  # ADR-46 slice 3 — negative (missing) dependencies: a consumer that
118
117
  # looked up a name and resolved nothing must be re-checked when that
119
118
  # name later appears (e.g. a `call.unresolved-toplevel` whose target
120
119
  # is defined by a later edit).
121
- @missing = {} # consumer => Set<"kind:name"> it looked up and missed
122
- @negative_dependents = {} # "kind:name" => Set<consumer> (inverted @missing)
123
- @class_decls = {} # path => Set<qualified class name declared in the file>
120
+ @missing = {} # consumer => the "kind:name" keys it looked up and missed
121
+ @negative_dependents = {} # "kind:name" => consumers (inverted @missing)
122
+ @class_decls = {} # path => qualified class names declared in the file
124
123
  # Issue #644 — the value-constant twin: path => { qualified constant name => publication descriptor }.
125
124
  # Diffed on a recheck to find every constant whose PUBLISHED answer could have moved, which satisfies
126
125
  # the `constant:` edges a reader records at every reference, resolved or not.
@@ -139,6 +138,9 @@ module Rigor
139
138
  # the frozen `Cache::Descriptor::FileEntry` / `GlobEntry` rows, as each read observed its input. A recheck
140
139
  # re-analyses every file one of whose rows no longer validates ({#plugin_read_affected}).
141
140
  @plugin_reads = {}
141
+ # Issue #2020 — the run-scoped plugin rows each analysed file registered, and the reads that make one stale.
142
+ @plugin_disclosures = RunDisclosureLedger.new
143
+ @stale_disclosures = Set.new
142
144
  reset_carried_state
143
145
  # ADR-88 WD1 — the plugin fact-surface digest computed for THIS invocation (nil until a
144
146
  # `#run_incremental` pass runs / a plugin-free project) and the reporting flags a caller (the CLI
@@ -269,8 +271,10 @@ module Rigor
269
271
  @effect_collections = runner.effect_collections_by_path
270
272
  @effects_identity = current_effects_identity
271
273
  @effect_plugin_digest = effect_plugin_digest_of(runner)
274
+ @synthetic_surface_digest = synthetic_surface_digest_of(runner)
272
275
  @cache = per_file(runner.per_file_diagnostics)
273
276
  @plugin_reads = plugin_reads_of(runner, @analyzed)
277
+ @plugin_disclosures = RunDisclosureLedger.from(runner.analysis_disclosures.slice(*@analyzed))
274
278
  record_baseline_digests
275
279
  @run_level_rows = runner.run_level_rows
276
280
  diagnostics
@@ -294,16 +298,11 @@ module Rigor
294
298
  fresh_params = fresh_param_table(current, changed, added, removed)
295
299
  param_files, param_pairs = param_seed_invalidation(fresh_params)
296
300
  affected = affected_closure(changed, added, removed, param_files, param_pairs)
297
- forced, analyze_set = forced_analyze_set(affected, current)
298
- reused = (current & previous) - analyze_set.to_a
299
- # The freshly collected table is handed to the runner so the run seeds from the SAME table the diff
300
- # was decided on (and the collector runs once per recheck, not twice). The reused files' cached rows
301
- # go in too, so the runner lists them in the full run's file order (#1524).
302
- runner = build_runner(analyze_only: analyze_set, record_dependencies: true,
303
- param_inferred_types: fresh_params, **forced_target_options(forced),
304
- restored_run_level_rows: @run_level_rows, **served_state(reused))
305
- merged = run_runner(runner).diagnostics
306
- return rebaseline_recheck(changed, added, removed) if served_effects_stale?(runner, reused)
301
+ affected, analyze_set, runner, merged, reused =
302
+ run_settled_closure(affected, [current, previous, removed], fresh_params)
303
+ if served_effects_stale?(runner, reused) || synthetic_surface_moved?(runner, reused)
304
+ return rebaseline_recheck(changed, added, removed)
305
+ end
307
306
 
308
307
  absorb(runner, current, analyze_set, removed)
309
308
  @param_table = fresh_params
@@ -311,6 +310,36 @@ module Rigor
311
310
  stale_readers: @stale_plugin_readers)
312
311
  end
313
312
 
313
+ # {#recheck}'s narrowed run. Issue #2020 — a stale run-scoped key whose chosen refresher registered nothing (a
314
+ # parse error, a plugin that raised, an edit that took the notice out of its text) is decided by its other
315
+ # holders, so they join the closure and it runs again. Returns `[affected, analyze_set, runner, merged, reused]`.
316
+ def run_settled_closure(affected, (current, previous, removed), fresh_params)
317
+ forced, analyze_set = forced_analyze_set(affected, current)
318
+ runner, merged, reused = run_closure(analyze_set, current, previous, forced, fresh_params)
319
+ fallback = @plugin_disclosures.unresolved_holders(runner.analysis_disclosures, analyze_set, @stale_disclosures,
320
+ removed.to_set)
321
+ return [affected, analyze_set, runner, merged, reused] if fallback.empty?
322
+
323
+ affected = (affected | fallback).freeze
324
+ @stale_plugin_readers |= fallback
325
+ forced, analyze_set = forced_analyze_set(affected, current)
326
+ [affected, analyze_set, *run_closure(analyze_set, current, previous, forced, fresh_params)]
327
+ end
328
+ private :run_settled_closure
329
+
330
+ # One narrowed run of {#recheck}'s closure: the runner, its merged rows, and the files it served. The freshly
331
+ # collected table is handed to the runner so the run seeds from the SAME table the diff was decided on (and the
332
+ # collector runs once per recheck, not twice). The reused files' cached rows go in too, so the runner lists them
333
+ # in the full run's file order (#1524).
334
+ def run_closure(analyze_set, current, previous, forced, fresh_params)
335
+ reused = (current & previous) - analyze_set.to_a
336
+ runner = build_runner(analyze_only: analyze_set, record_dependencies: true,
337
+ param_inferred_types: fresh_params, **forced_target_options(forced),
338
+ restored_run_level_rows: @run_level_rows, **served_state(reused))
339
+ [runner, run_runner(runner).diagnostics, reused]
340
+ end
341
+ private :run_closure
342
+
314
343
  # Editor mode option B (#146) — a whole-project recheck with the editor's buffer substituted for one
315
344
  # file, for the CLI's `--incremental --tmp-file=X --instead-of=Y`. Returns the {Recheck} when the
316
345
  # snapshot could be reused, or nil when it could not — the caller then falls back to option A
@@ -361,7 +390,7 @@ module Rigor
361
390
  # records none at all), so no dependent set can bound who read it. An edit that moved it re-analyses the
362
391
  # whole project — what a `sig/` edit already costs through the snapshot fingerprint.
363
392
  def affected_closure(changed, added, removed, param_files = Set.new, param_pairs = Set.new)
364
- @stale_plugin_readers = plugin_read_affected
393
+ @stale_plugin_readers = plugin_read_affected | disclosure_refreshers(removed)
365
394
  return whole_project_closure(changed, added) if source_rbs_gate.moved?(@seed_bundles, changed, added, removed)
366
395
 
367
396
  (gated_closure(changed, added, removed, param_files, param_pairs) | @stale_plugin_readers).freeze
@@ -471,8 +500,8 @@ module Rigor
471
500
  # exempts such a write is a fact of the whole program — an alias, or a `write` / `to_str` / `to_int` / hatch
472
501
  # defined in any spelling on any receiver, a rewritten ancestor, a mixin — which no name-keyed edge can
473
502
  # follow, so a stale report would outlive the edit that exempts it. Such files are rare, so this costs little.
474
- # A file the census silenced is not re-checked when the exemption goes away: that stale answer misses a
475
- # report, it never makes one.
503
+ # A file the census silenced is re-checked through the `global_census:` keys its rule recorded
504
+ # ({#global_census_affected}, issue #2158).
476
505
  def global_write_reporters
477
506
  @cache.each_with_object(Set.new) do |(path, diagnostics), reporters|
478
507
  reporters << path if diagnostics.any? { |diagnostic| diagnostic.rule.to_s.start_with?("global.") }
@@ -602,9 +631,21 @@ module Rigor
602
631
  runner = build_runner
603
632
  files = @paths ? runner.analysis_file_set(@paths) : runner.analysis_file_set
604
633
  rebind_buffer(files)
605
- files
634
+ admit_new_buffer(files)
606
635
  end
607
636
 
637
+ # Issue #2233 — a buffer for a file that is not on disk (a new file) is in no expansion, so the set above never
638
+ # names it: the recheck neither analyses it nor sees its declarations, and the run answers "re-analysed 0 file(s)"
639
+ # with no diagnostics for the buffer. Option A (single-file scope) analyses the logical path whether or not it
640
+ # exists; option B joins it to the set the same way, as an added file, so the closure analyses it like any new
641
+ # file. A buffer already matched by {#rebind_buffer} is in `files` and is returned as it is.
642
+ def admit_new_buffer(files)
643
+ return files if @buffer.nil? || files.include?(@buffer.logical_path)
644
+
645
+ files + [@buffer.logical_path]
646
+ end
647
+ private :admit_new_buffer
648
+
608
649
  # Verification engine (the `--verify-incremental` gate): with NO source edit, re-analyze `subset` fresh
609
650
  # and serve every other analyzed file from the baseline cache. Because nothing on disk changed, the
610
651
  # merged result MUST equal a full analysis — so this exercises the subset-analysis and cache-merge
@@ -620,6 +661,32 @@ module Rigor
620
661
  run_runner(runner).diagnostics
621
662
  end
622
663
 
664
+ # Issue #1967 — the edit replay of `--verify-incremental-edits`: a warm recheck of this session's baseline with
665
+ # `buffer` standing in for one file, the way editor mode option B (#146) rechecks an unsaved edit. The baseline is
666
+ # handed over as a Marshal copy of the snapshot a `--incremental` run would save, through a fresh session that
667
+ # never persists, so this session's own state is untouched and each replay starts from the same baseline. Unlike
668
+ # {#reanalyze_subset}, whose tree is the baseline's, the edit moves what the served files' rows were computed
669
+ # from, so a dependent the recheck fails to re-analyse is served stale and the comparison sees it. Returns the
670
+ # {Recheck}, or nil when the recheck declined as option B does (the plugin fact surface moved, an unverified
671
+ # source-RBS gate, a re-baseline), where the answer would be a full analysis and there is nothing to compare.
672
+ def replay_edit(buffer)
673
+ adopt_plugin_fact_fingerprint if @plugin_fact_reusable.nil?
674
+ replay = IncrementalSession.new(
675
+ configuration: @configuration, paths: @paths, environment: @environment, cache_store: @cache_store,
676
+ plugin_requirer: @plugin_requirer, buffer: buffer, explain: @explain
677
+ )
678
+ replay.run_buffer_recheck(snapshot: ReplaySnapshot.new(blob: Marshal.dump(to_payload)), fingerprint: :replay)
679
+ end
680
+
681
+ # The in-memory stand-in for {Cache::IncrementalSnapshot} {#replay_edit} hands its replay session: every load is a
682
+ # fresh copy, as a load from disk is.
683
+ ReplaySnapshot = Data.define(:blob) do
684
+ def load(fingerprint:) # rubocop:disable Lint/UnusedMethodArgument
685
+ Marshal.load(blob) # rubocop:disable Security/MarshalLoad
686
+ end
687
+ end
688
+ private_constant :ReplaySnapshot
689
+
623
690
  # Cross-process incremental run (the `--incremental` flag's engine). With a disk `snapshot` whose
624
691
  # `fingerprint` matches, restore the prior per-file state and `#recheck` (re-analyze only the changed
625
692
  # closure, serve the rest from the restored cache); otherwise run a full `#baseline`. Either way,
@@ -991,7 +1058,7 @@ module Rigor
991
1058
  restore_structural_declarations(payload)
992
1059
  @return_summaries = payload.return_summaries || {}
993
1060
  @param_table = payload.param_table || {} # ADR-67 WD6c lift — the seeds the cache was built under.
994
- @plugin_reads = payload.plugin_reads # Issue #1553 — `{}` in a payload built without it.
1061
+ restore_plugin_records(payload)
995
1062
  # Issues #796 / #794 — absent (nil) in a snapshot written before schema 22 is impossible (the SCHEMA
996
1063
  # gate rejects one), so nil here means the writing run genuinely had nothing to say.
997
1064
  @run_level_rows = payload.run_level_rows
@@ -1007,6 +1074,14 @@ module Rigor
1007
1074
  @negative_dependents = Incremental.invert(@missing)
1008
1075
  end
1009
1076
 
1077
+ # Issue #1553 — the plugin reads each file's last analysis made, and (#2020) the run-scoped plugin rows it
1078
+ # registered; `{}` in a payload built without them. Extracted to keep {#restore} under its ABC budget.
1079
+ def restore_plugin_records(payload)
1080
+ @plugin_reads = payload.plugin_reads
1081
+ @plugin_disclosures = RunDisclosureLedger.load(payload.plugin_disclosures)
1082
+ @synthetic_surface_digest = payload.synthetic_surface_digest # the plugin-built ADR-16 index (PR #2286 M1)
1083
+ end
1084
+
1010
1085
  # ADR-46 slice 3 / issue #644 — the structural tier's negative-edge state: the per-consumer missed
1011
1086
  # lookups plus the two per-file DECLARATION sets (`class_decls`, `constant_decls`) whose diff says what
1012
1087
  # APPEARED in an edit. Absent in an older snapshot → empty, which only loses the appeared-symbol
@@ -1032,7 +1107,9 @@ module Rigor
1032
1107
  effects_identity: @effects_identity,
1033
1108
  run_level_rows: @run_level_rows,
1034
1109
  effect_plugin_digest: @effect_plugin_digest,
1110
+ synthetic_surface_digest: @synthetic_surface_digest,
1035
1111
  plugin_reads: @plugin_reads,
1112
+ plugin_disclosures: @plugin_disclosures.to_h,
1036
1113
  discovery_digests: @discovery_digests
1037
1114
  )
1038
1115
  end
@@ -1152,11 +1229,13 @@ module Rigor
1152
1229
  @digests[path] = pack_digest(path)
1153
1230
  fresh_reads.key?(path) ? @plugin_reads[path] = fresh_reads[path] : @plugin_reads.delete(path)
1154
1231
  end
1232
+ absorb_disclosures(runner, analyze_set)
1155
1233
  forget_unbound_digests
1156
1234
  absorb_dependency_graph(runner)
1157
1235
  # Issues #796 / #794 — the recheck's own snapshots already carry the replayed rows folded together
1158
1236
  # with anything its closure demanded for itself, so this is the same union the next run replays.
1159
1237
  @run_level_rows = runner.run_level_rows
1238
+ @synthetic_surface_digest = synthetic_surface_digest_of(runner)
1160
1239
  refresh_return_summaries(runner, analyze_set)
1161
1240
  refresh_effect_collections(runner, analyze_set)
1162
1241
  end
@@ -1200,6 +1279,7 @@ module Rigor
1200
1279
  @digests.delete(path)
1201
1280
  @sources.delete(path)
1202
1281
  @plugin_reads.delete(path)
1282
+ @plugin_disclosures.forget(path) # Unpinned hygiene: a removed file is never served, so never replayed.
1203
1283
  @symbol_sources.delete(path)
1204
1284
  @ancestry_sources.delete(path)
1205
1285
  @missing.delete(path)
@@ -1271,7 +1351,8 @@ module Rigor
1271
1351
  # top-level def, `method:C#m` otherwise), then unions the recorded negative-dependents of those keys.
1272
1352
  # Issue #1120 — plus the consumers of any refinement name the edit moved ({#refinement_affected}). Issue #1367 —
1273
1353
  # and every file whose last result holds a `global.*` diagnostic ({#global_write_reporters}). Issue #1459 — and
1274
- # the consumers of any method name a file started or stopped defining ({#defined_name_affected}).
1354
+ # the consumers of any method name a file started or stopped defining ({#defined_name_affected}). Issue #2134 —
1355
+ # and the consumers of any visibility slot a file moved ({#visibility_name_affected}).
1275
1356
  def negative_affected(changed, removed, new_fingerprints, new_class_decls, new_constant_decls, summary = nil)
1276
1357
  appeared_methods = Incremental.appeared_symbols(changed, @symbol_fingerprints, new_fingerprints)
1277
1358
  # Issue #639 — a class DISAPPEARING satisfies the `class:` kind too, now that a resolved bare
@@ -1288,11 +1369,18 @@ module Rigor
1288
1369
  keys = appeared_methods.map { |symbol| negative_key_for(symbol) }
1289
1370
  keys.concat(class_move_keys(moved_classes))
1290
1371
  keys.concat(moved_constants.map { |name| "constant:#{name.split('::').last}" })
1291
- Incremental.negative_closure(keys, @negative_dependents) | refinement_affected(changed + removed, summary) |
1292
- defined_name_affected(changed + removed, summary) | mark_affected(changed + removed, summary) |
1372
+ Incremental.negative_closure(keys, @negative_dependents) | table_diff_affected(changed + removed, summary) |
1293
1373
  global_write_reporters
1294
1374
  end
1295
1375
 
1376
+ # The consumers of the name-keyed edges whose producers diff a per-file table between each file's seed bundle
1377
+ # and the scan.
1378
+ def table_diff_affected(paths, summary)
1379
+ refinement_affected(paths, summary) | defined_name_affected(paths, summary) |
1380
+ visibility_name_affected(paths, summary) | mark_affected(paths, summary) |
1381
+ global_census_affected(paths, summary) | ivar_set_affected(paths, summary)
1382
+ end
1383
+
1296
1384
  # Issue #1833 — the consumers whose answer read a class mark that moved in this edit. A reader of a class's
1297
1385
  # envelope bucket records `mark:<last segment>` (`Scope#parameter_envelopes_of`), whichever way it answered, so
1298
1386
  # a file that adds or drops only a mark (`x.singleton_class.prepend(Comparable)`, `o.extend(Helper)` in a
@@ -1319,7 +1407,8 @@ module Rigor
1319
1407
  keys = moved.map { |(class_name, _mark)| "mark:#{class_name.split('::').last}" }.uniq
1320
1408
  keys.concat(mixin_keys(tainted))
1321
1409
  extended = Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK
1322
- keys << "mark:*" if moved.any? { |(_class_name, mark)| mark == extended }
1410
+ meta = Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK
1411
+ keys << "mark:*" if moved.any? { |(_class_name, mark)| mark == extended || mark == meta }
1323
1412
  Incremental.negative_closure(keys, @negative_dependents)
1324
1413
  end
1325
1414
 
@@ -1379,8 +1468,55 @@ module Rigor
1379
1468
  Incremental.negative_closure(names.map { |name| "defines:#{name}" }, @negative_dependents)
1380
1469
  end
1381
1470
 
1471
+ # Issue #2158 — the consumers whose `global.*` write rule read a census entry that a file in `paths` started or
1472
+ # stopped writing. The before-state is each file's seed bundle, the after-state the scan's per-file census. The
1473
+ # write the census silenced reports nothing, so {#global_write_reporters} does not reach it.
1474
+ def global_census_affected(paths, summary)
1475
+ after = (summary && summary[:global_write_census]) || {}
1476
+ before = paths.to_h { |path| [path, @seed_bundles.dig(path, :global_write_census)] }
1477
+ keys = Incremental.changed_global_census_keys(paths, before, after)
1478
+ return Set.new if keys.empty?
1479
+
1480
+ Incremental.negative_closure(keys, @negative_dependents)
1481
+ end
1482
+
1483
+ # Issue #1154 — the consumers whose declared-parameter ivar credit read an ivar writer entry a file in `paths`
1484
+ # started or stopped recording ({GlobalWriteCensus.ivar_key}: `ivar_set:@x`, `ivar_set_class:K`,
1485
+ # `own_write:K#@x`). The before-state is each file's seed bundle, the after-state the scan's per-file census. The
1486
+ # write sits in a method body, so neither the declaration signature nor the symbol fingerprints move with it.
1487
+ def ivar_set_affected(paths, summary)
1488
+ after = (summary && summary[:global_write_census]) || {}
1489
+ keys = Set.new
1490
+ paths.each do |path|
1491
+ census = @seed_bundles.dig(path, :global_write_census) || Inference::GlobalWriteCensus::EMPTY
1492
+ before = Inference::GlobalWriteCensus.ivar_set_entries(census)
1493
+ now = Inference::GlobalWriteCensus.ivar_set_entries(after[path] || Inference::GlobalWriteCensus::EMPTY)
1494
+ (before ^ now).each { |entry| keys << Inference::GlobalWriteCensus.ivar_key(entry) }
1495
+ end
1496
+ return Set.new if keys.empty?
1497
+
1498
+ Incremental.negative_closure(keys.to_a, @negative_dependents)
1499
+ end
1500
+
1501
+ # Issue #2134 — the consumers whose answer read a class's visibility slot for a method, or its contest, for each
1502
+ # slot whose entries a file in `paths` added, dropped or changed (`Scope#discovered_method_visibility`). The
1503
+ # before-state is each file's seed bundle, the after-state the scan's per-file entries. A `private :foo` in a
1504
+ # reopening or a `class_eval` block moves no `def`, so neither the symbol fingerprints nor the defined-name diff
1505
+ # sees it.
1506
+ def visibility_name_affected(paths, summary)
1507
+ after = (summary && summary[:visibility_entries]) || {}
1508
+ before = paths.to_h do |path|
1509
+ bundle = @seed_bundles[path]
1510
+ [path, bundle && Inference::ScopeIndexer.visibility_entries(bundle)]
1511
+ end
1512
+ slots = Incremental.changed_visibility_slots(paths, before, after)
1513
+ return Set.new if slots.empty?
1514
+
1515
+ Incremental.negative_closure(slots.map { |slot| "visibility:#{slot}" }, @negative_dependents)
1516
+ end
1517
+
1382
1518
  # The qualified class/module names declared in the pre-parsed `index` (shared with
1383
- # {#symbol_fingerprints_from_index}, so the changed set is parsed once). `{ path => Set<class name> }`.
1519
+ # {#symbol_fingerprints_from_index}, so the changed set is parsed once), keyed by path.
1384
1520
  def class_declarations_from_index(index)
1385
1521
  return {} if index.nil?
1386
1522
 
@@ -1541,7 +1677,22 @@ module Rigor
1541
1677
  # and (#1898) their effect collections, so the run closes its effect graph over the whole project.
1542
1678
  def served_state(reused)
1543
1679
  { served_per_file_diagnostics: @cache.slice(*reused),
1544
- served_effect_collections: @effect_collections.slice(*reused) }
1680
+ served_effect_collections: @effect_collections.slice(*reused),
1681
+ served_run_disclosures: @plugin_disclosures.served(reused, @stale_disclosures) }
1682
+ end
1683
+
1684
+ # Issue #2020 — folds the run's run-scoped registrations into the ledger, refreshing the keys this recheck found
1685
+ # stale. Extracted to keep {#absorb} under its ABC budget.
1686
+ def absorb_disclosures(runner, analyze_set)
1687
+ @plugin_disclosures.absorb(runner.analysis_disclosures, analyze_set, @stale_disclosures)
1688
+ @stale_disclosures = Set.new
1689
+ end
1690
+
1691
+ # Issue #2020 — the run-scoped plugin rows whose reads went stale ({RunDisclosureLedger}): one registering file
1692
+ # each joins the closure, and their replays are withheld until {#absorb} refreshes them from that file.
1693
+ def disclosure_refreshers(removed)
1694
+ @stale_disclosures = @plugin_disclosures.stale_identities { |row| plugin_read_fresh?(row) }
1695
+ @plugin_disclosures.refreshers(@stale_disclosures, removed.to_set)
1545
1696
  end
1546
1697
 
1547
1698
  # Issue #1898 — the plugin effect-fact digest a run's collections were stamped under. A plugin writes its
@@ -1560,6 +1711,21 @@ module Rigor
1560
1711
  effect_plugin_digest_of(runner) != @effect_plugin_digest
1561
1712
  end
1562
1713
 
1714
+ # PR #2286 review M1 — whether the ADR-16 synthetic-method index `runner` built differs from the one the
1715
+ # served files were analysed under. The index comes from class-body DSL calls (`devise :confirmable`,
1716
+ # a Tier C `attribute`), which no declaration signature records and no dependency edge names, so a file
1717
+ # that dispatched through a synthesised member is not in the closure of the edit that removed it. The
1718
+ # comparison waits for the run because the index is built by its pre-pass; a move re-analyses the project.
1719
+ def synthetic_surface_moved?(runner, reused)
1720
+ return false if reused.empty?
1721
+
1722
+ synthetic_surface_digest_of(runner) != @synthetic_surface_digest
1723
+ end
1724
+
1725
+ def synthetic_surface_digest_of(runner)
1726
+ runner.synthetic_method_index&.surface_digest
1727
+ end
1728
+
1563
1729
  # The answer {#recheck} gives when {#served_effects_stale?}: a full baseline, reported as a recheck that
1564
1730
  # re-analysed every file. {#run_incremental} reads `rebaselined` so it neither runs a second baseline
1565
1731
  # nor reports the run as warm.
@@ -1644,19 +1810,12 @@ module Rigor
1644
1810
  def rebind_buffer(files)
1645
1811
  return if @buffer.nil? || files.include?(@buffer.logical_path)
1646
1812
 
1647
- target = real_path(@buffer.logical_path)
1648
- return if target.nil?
1813
+ target = BufferBinding.real_path(@buffer.logical_path)
1649
1814
 
1650
- match = files.find { |path| real_path(path) == target }
1815
+ match = files.find { |path| BufferBinding.real_path(path) == target }
1651
1816
  @buffer = BufferBinding.new(logical_path: match, physical_path: @buffer.physical_path) if match
1652
1817
  end
1653
1818
 
1654
- def real_path(path)
1655
- File.realpath(path)
1656
- rescue StandardError
1657
- nil
1658
- end
1659
-
1660
1819
  # True when `path`'s recorded stat entry proves it unchanged since the last analysis. Any stat / parse
1661
1820
  # failure (missing entry, unreadable / vanished file) reads as NOT fresh (→ re-analyse), preserving the
1662
1821
  # prior `digest(path) != recorded` "changed" semantics for a file that cannot be validated.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../rooted_glob"
4
+ require_relative "diagnostic"
4
5
 
5
6
  module Rigor
6
7
  module Analysis
@@ -10,8 +11,13 @@ module Rigor
10
11
  # is taken verbatim; anything else contributes no files here (the Runner layers its own path-error
11
12
  # diagnostics and buffer / in-memory handling on top — none of which add or remove files for an ordinary
12
13
  # on-disk check, so the two file sets agree).
14
+ #
15
+ # #1991 — also the one place a path that names neither a directory nor a `.rb` file is worded and
16
+ # severity-graded, so `rigor check` and `rigor sig-gen` report the same path error the same way.
13
17
  module PathExpansion
14
18
  RUBY_GLOB = "**/*.rb"
19
+ NO_SUCH_PATH = "no such file or directory"
20
+ NOT_A_RUBY_FILE = "not a Ruby file (expected `.rb` or a directory)"
15
21
 
16
22
  module_function
17
23
 
@@ -28,10 +34,45 @@ module Rigor
28
34
  end
29
35
  end
30
36
 
37
+ # The `[path, message]` pairs for each of `paths` that names neither a directory nor a `.rb` file,
38
+ # worded as `rigor check` words them.
39
+ def unusable(paths)
40
+ Array(paths).each_with_object([]) do |path, bad|
41
+ next if File.directory?(path) || (File.file?(path) && path.end_with?(".rb"))
42
+
43
+ bad << [path, unusable_message(path)]
44
+ end
45
+ end
46
+
47
+ def unusable_message(path)
48
+ File.exist?(path) ? NOT_A_RUBY_FILE : NO_SUCH_PATH
49
+ end
50
+
51
+ # The error diagnostics for `bad` (from {unusable}). A bad path among valid ones is a warning that it was
52
+ # skipped; a bad path that leaves nothing to analyse is an error, so a lone typo is not masked.
53
+ def diagnostics(bad, any_files:)
54
+ severity = any_files ? :warning : :error
55
+ suffix = any_files ? " (skipped)" : ""
56
+ bad.map do |path, message|
57
+ Diagnostic.new(path: path, line: 1, column: 1, message: "#{message}#{suffix}", severity: severity)
58
+ end
59
+ end
60
+
31
61
  def directory_files(dir, exclude_patterns, base: Dir.pwd)
32
62
  reject_excluded(RootedGlob.glob(dir, RUBY_GLOB), exclude_patterns, base: base)
33
63
  end
34
64
 
65
+ # The configured project files, expanded as `rigor check` expands its configured paths: a directory globs,
66
+ # any other entry (a file, or a path that does not exist) is kept verbatim and left to the reader to skip.
67
+ # The file list a cross-file discovery seed is folded over ({Protection::DiscoverySeed}).
68
+ def configured_files(configuration)
69
+ Array(configuration.paths).flat_map do |path|
70
+ next [path] unless File.directory?(path)
71
+
72
+ directory_files(path, configuration.exclude_patterns, base: configuration.exclude_base)
73
+ end
74
+ end
75
+
35
76
  # `exclude_patterns` are matched with `File.fnmatch?` WITHOUT `FNM_PATHNAME`, so `**`/`*` span separators
36
77
  # (the patterns behave like substring globs) — the exact semantics the Runner's directory glob applies.
37
78
  #
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Analysis
7
+ # Issue #2021 — the files a run analyses outside the configured `paths:` load through `require_relative`.
8
+ #
9
+ # A run over path arguments widens its discovery to the configured paths (#684), which is the whole project for
10
+ # a file inside them. A file outside them has no such project: `rigor check plugins/x/lib/rigor/plugin/x.rb`
11
+ # in a repository whose `paths:` is `lib` discovered `x.rb` and `lib` alone. The plugin's nested `Effects`,
12
+ # declared in the sibling `x/effects.rb`, was then invisible, the lexical lookup fell through to the
13
+ # project's `Rigor::Effects`, and every call on it drew a false `call.undefined-method` that checking the
14
+ # plugin's `lib` did not.
15
+ #
16
+ # The file names its siblings itself: Ruby loads what it `require_relative`s before any of its code runs, so
17
+ # those declarations are in scope at runtime. {.closure} follows that graph from the out-of-paths files,
18
+ # through each target that is itself outside the configured paths. A target under a configured path is
19
+ # already discovered and stops the walk there, so the files inside `paths:` are discovered exactly as a run
20
+ # over the configured paths discovers them. Only a string literal counts (`require_relative "a/b"`, with or
21
+ # without the `.rb`); a computed name, `require`, `load` and `autoload` are not followed, so a gem's own
22
+ # files and a Zeitwerk-loaded tree are not reached this way.
23
+ #
24
+ # The walk sees one entry point's load graph, and a file it does not reach may redefine what a walked file
25
+ # defines. The runner therefore discovers the walked files as uncertain: their declarations resolve constants
26
+ # and their methods exist, but no return type, arity or visibility is read from them.
27
+ module RequireRelativeClosure
28
+ # The walk's answer: the existing targets it reached, and the targets it named that are not files (each an
29
+ # input of the answer too, since one appearing would join the discovery).
30
+ Result = Data.define(:found, :missing)
31
+
32
+ module_function
33
+
34
+ # @param seeds — the analysed files outside the configured paths, as spelled by the run.
35
+ # @param covered — `->(real_path) { Boolean }`, whether a target lies under a configured path, where discovery
36
+ # holds it already. The walk neither adds nor follows such a target. Paths are compared as real paths, so a
37
+ # symlinked spelling of a configured file is covered too.
38
+ # @param read — `->(path) { String }`, the source of a file (an editor buffer's bytes for its file).
39
+ # @return a {Result}; `found` holds absolute paths in walk order, each file once, and never a seed.
40
+ def closure(seeds, covered:, read:)
41
+ seen = seeds.to_set { |path| real_path(path) }
42
+ queue = seeds.dup
43
+ found = []
44
+ missing = []
45
+ until queue.empty?
46
+ targets_of(queue.shift, read).each do |target|
47
+ next missing << target unless File.file?(target)
48
+
49
+ real = real_path(target)
50
+ next if covered.call(real) || !seen.add?(real)
51
+
52
+ found << target
53
+ queue << target
54
+ end
55
+ end
56
+ Result.new(found: found, missing: missing.uniq)
57
+ end
58
+
59
+ def real_path(path)
60
+ File.realpath(path)
61
+ rescue SystemCallError
62
+ File.absolute_path(path)
63
+ end
64
+
65
+ # The `.rb` paths `path` names with a literal `require_relative`, absolute, whether or not they exist.
66
+ def targets_of(path, read)
67
+ source = read.call(path)
68
+ return [] if source.nil?
69
+
70
+ base = File.dirname(File.absolute_path(path))
71
+ literal_features(Prism.parse(source).value).map do |feature|
72
+ File.expand_path(feature.end_with?(".rb") ? feature : "#{feature}.rb", base)
73
+ end
74
+ rescue StandardError
75
+ []
76
+ end
77
+
78
+ # In source order, so discovery meets the targets in the order Ruby loads them.
79
+ def literal_features(root)
80
+ calls = []
81
+ stack = [root]
82
+ until stack.empty?
83
+ node = stack.pop
84
+ calls << node if literal_feature(node)
85
+ stack.concat(node.compact_child_nodes)
86
+ end
87
+ calls.sort_by { |call| call.location.start_offset }.filter_map { |call| literal_feature(call) }
88
+ end
89
+
90
+ def literal_feature(node)
91
+ return nil unless node.is_a?(Prism::CallNode) && node.name == :require_relative && node.receiver.nil?
92
+
93
+ arguments = node.arguments&.arguments
94
+ return nil unless arguments&.size == 1 && arguments.first.is_a?(Prism::StringNode)
95
+
96
+ arguments.first.unescaped
97
+ end
98
+ end
99
+ end
100
+ end
@@ -25,7 +25,7 @@ module Rigor
25
25
  # profile, taken from `Configuration::SeverityProfile::PROFILES`.
26
26
  # - `evidence_tier` — `:high` / `:medium` / `:low` (or `nil` for informational helpers), Rigor's own
27
27
  # confidence that a firing is a true positive, derived from the rule's firing gates. `:high` rules
28
- # fire only on a concrete, statically- known type with no metaprogramming escape (Rigor's
28
+ # fire only on a concrete, statically-known type with no metaprogramming escape (Rigor's
29
29
  # false-positive discipline has already filtered the uncertain cases); `:medium` rules rest on flow /
30
30
  # inference proofs that inherit a documented FP envelope (loop / mutation / RBS-strictness modelling
31
31
  # gaps); `:low` rules are resolution- or coverage-gap signals where a firing often reflects missing
@@ -706,7 +706,11 @@ module Rigor
706
706
  "Writes are in a singleton body — `def self.foo` or a `class << self` def — those track " \
707
707
  "separately.",
708
708
  "Writes are in a `def` inside an anonymous-class factory block (`Class.new do … end`, " \
709
- "`Module.new`, `Struct.new`, `Data.define`): that body defines another class."
709
+ "`Module.new`, `Struct.new`, `Data.define`): that body defines another class.",
710
+ "The class descends from a plugin-declared per-invocation class (ADR-125; rigor-actionpack declares " \
711
+ "`ActionController::Base` / `ActionController::API`, rigor-actionmailer `ActionMailer::Base`), and the " \
712
+ "write and every conflicting write sit in different actions: public methods no callback macro or " \
713
+ "`helper_method` names. One action runs per instance."
710
714
  ],
711
715
  suppression: "`# rigor:disable ivar-write-mismatch` on the offending write.",
712
716
  severity_authored: :error,
@@ -1116,7 +1120,7 @@ module Rigor
1116
1120
 
1117
1121
  module_function
1118
1122
 
1119
- # Looks up a rule by canonical id, legacy alias, or family wildcard. Returns an Array<Entry>:
1123
+ # Looks up a rule by canonical id, legacy alias, or family wildcard. Returns the matching entries:
1120
1124
  #
1121
1125
  # - canonical id → 1-element array,
1122
1126
  # - legacy alias → 1-element array (resolved to canonical),