rigortype 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -16,7 +16,24 @@ module Rigor
16
16
  # The audit deliberately mirrors the loader's own acceptance test (`path.directory?` + a
17
17
  # recursive `**/*.rbs` glob) so a `:ok` verdict means the loader did load from it and a
18
18
  # warning means it did not.
19
+ #
20
+ # {.bundled_plugin_routes} answers the mirror-image question — an entry that loads too
21
+ # *little of the plugin*, rather than nothing at all. See its own comment.
19
22
  module SignaturePathAudit
23
+ # The `plugins/` tree of the engine running this code, anchored from THIS file
24
+ # (`<root>/lib/rigor/signature_path_audit.rb`) so a git checkout and an installed
25
+ # `rigortype` gem answer identically — the anchor {Plugin::Loader::ENGINE_ROOT} uses, for
26
+ # the reason it documents.
27
+ #
28
+ # Deliberately re-derived rather than read off `Plugin::Loader`: ADR-87 WD4 keeps this
29
+ # file, which every `rigor check` loads before the cache probe, off the plugin
30
+ # subsystem's require graph — and that graph is order-dependent (`plugin/registry.rb`
31
+ # resolves `NodeRuleWalk` at load, which only `rigor/plugin.rb` has required by then), so
32
+ # requiring the loader from here fails outright. `signature_path_audit_spec` pins the two
33
+ # anchors equal, and pins this discovery against `Loader.bundled_plugin_sig_path`, so the
34
+ # duplication cannot drift unnoticed.
35
+ BUNDLED_PLUGINS_ROOT = File.expand_path("../../plugins", __dir__).freeze
36
+
20
37
  # One configured `signature_paths:` entry's resolution status.
21
38
  #
22
39
  # `status` is one of:
@@ -60,17 +77,11 @@ module Rigor
60
77
  # `nil` — the unset default, where Rigor auto-detects `<root>/sig` — to get an empty
61
78
  # result: an absent auto-detected `sig/` is a normal setup, not a misconfiguration, so it
62
79
  # is never audited.
63
- #
64
- # @param signature_paths [Array<String, Pathname>, nil]
65
- # @return [Array<Entry>]
66
80
  def self.audit(signature_paths)
67
81
  Array(signature_paths).map { |path| classify(path.to_s) }
68
82
  end
69
83
 
70
84
  # The subset of {audit} that resolved to nothing — the entries worth warning about.
71
- #
72
- # @param signature_paths [Array<String, Pathname>, nil]
73
- # @return [Array<Entry>]
74
85
  def self.warnings(signature_paths)
75
86
  audit(signature_paths).select(&:warning?)
76
87
  end
@@ -82,5 +93,141 @@ module Rigor
82
93
  count = Dir.glob(File.join(path, "**", "*.rbs")).size
83
94
  Entry.new(path: path, status: count.zero? ? :empty : :ok, rbs_file_count: count)
84
95
  end
96
+
97
+ # One configured entry that reaches the `sig/` of a plugin THIS engine bundles, while
98
+ # `plugins:` never names that plugin (issue #697).
99
+ BundledPluginRoute = Data.define(:path, :gem) do
100
+ # Names the plugin and the one edit that fixes it. The consequence is spelled out
101
+ # because the symptom the user actually sees is `call.undefined-method` on code that
102
+ # runs — nothing in that row points back at the `signature_paths:` line.
103
+ def message
104
+ "signature_paths: #{path.inspect} loads the signatures of the bundled plugin #{gem.inspect}, " \
105
+ "which `plugins:` does not name — so the plugin's manifest never loads, and the classes it " \
106
+ "declares only partially on purpose (ADR-26 `open_receivers:`) are read as complete, which " \
107
+ "reports methods your code really defines as undefined. " \
108
+ "Add #{gem.inspect} to `plugins:` instead of naming its `sig/` in `signature_paths:`."
109
+ end
110
+
111
+ def to_h
112
+ { "path" => path, "gem" => gem, "message" => message }
113
+ end
114
+ end
115
+
116
+ # The mirror image of {.warnings}: an entry the loader reads perfectly well, which
117
+ # nevertheless leaves the user worse off than not configuring it.
118
+ #
119
+ # A bundled plugin ships two halves that only work together — the RBS under its `sig/`,
120
+ # and the manifest that says which of those declarations are deliberately partial
121
+ # (ADR-26 `open_receivers:`). `plugins:` loads both; `signature_paths:` loads only the
122
+ # first, and the partial declarations then read as complete, so a scope the application
123
+ # really defines draws `call.undefined-method`. This is a configuration mistake with no
124
+ # visible cause, so it is reported here rather than fixed by teaching the check rules yet
125
+ # another route to the same protection — which is [#660](https://github.com/rigortype/rigor/issues/660)'s
126
+ # question to settle, not this warning's.
127
+ #
128
+ # **What it asks is what the entry LOADS, not what it looks like**, exactly as
129
+ # `Environment::RbsLoader.bundled_overlay_twin_signatures_loaded?` does for the overlay
130
+ # stand-down: both sides are `File.realpath`'d and matched against the plugin's actual
131
+ # `.rbs` files, so a symlink or a case-variant spelling answers the same as the directory
132
+ # itself, and an entry naming a file (or a directory that does not exist) — which the
133
+ # loader reads nothing from — matches nothing. The two are not single-homed because the
134
+ # overlay-side predicate lives on the RBS loader, and ADR-87 WD4 keeps this file, which
135
+ # every `rigor check` loads before the cache probe, off the engine's require graph.
136
+ #
137
+ # @param signature_paths — {Configuration#signature_paths}; `nil` (the unset default,
138
+ # where Rigor auto-detects `<root>/sig`) is never audited.
139
+ # @param plugins — the raw {Configuration#plugins} list.
140
+ def self.bundled_plugin_routes(signature_paths, plugins = [])
141
+ entries = resolved_entries(signature_paths)
142
+ return [] if entries.empty?
143
+
144
+ root = canonical_path(BUNDLED_PLUGINS_ROOT)
145
+ return [] if root.nil? || entries.none? { |(_, dir)| overlapping?(dir, root) }
146
+
147
+ listed = listed_plugin_names(plugins)
148
+ bundled_plugin_sig_dirs.reject { |gem, _| listed?(gem, listed) }.flat_map do |gem, sig_dir|
149
+ files = canonical_rbs_files(sig_dir)
150
+ next [] if files.empty?
151
+
152
+ entries.filter_map do |(entry, dir)|
153
+ BundledPluginRoute.new(path: entry, gem: gem) if files.any? { |file| under?(file, dir) }
154
+ end
155
+ end
156
+ end
157
+
158
+ # `[entry as configured, canonical directory]` for every entry the loader would read from.
159
+ def self.resolved_entries(signature_paths)
160
+ Array(signature_paths).filter_map do |entry|
161
+ dir = canonical_path(entry.to_s)
162
+ [entry.to_s, dir] if dir && File.directory?(dir)
163
+ end
164
+ end
165
+ private_class_method :resolved_entries
166
+
167
+ # Gem name of every plugin the engine bundles that ships signatures, to its `sig/`.
168
+ # Derived from the `plugins/` tree rather than listed, for {Plugin::FirstParty}'s reason:
169
+ # a list would be a second source of truth, and its first drift would silently stop
170
+ # warning about the plugin that drifted.
171
+ def self.bundled_plugin_sig_dirs
172
+ return {} unless File.directory?(BUNDLED_PLUGINS_ROOT)
173
+
174
+ Dir.children(BUNDLED_PLUGINS_ROOT).sort.filter_map do |gem|
175
+ sig = File.join(BUNDLED_PLUGINS_ROOT, gem, "sig")
176
+ [gem, sig] if File.directory?(sig)
177
+ end.to_h
178
+ end
179
+
180
+ # Every name a `plugins:` entry puts on the record, in both the bare-string and the
181
+ # `gem:` / `id:` hash forms. An `enabled: false` entry counts as named even though it
182
+ # loads nothing: the user has demonstrably found the plugin, and telling them to add what
183
+ # is already there would be worse than staying quiet. Under-warning is this module's
184
+ # standing FP-safe direction.
185
+ def self.listed_plugin_names(plugins)
186
+ Array(plugins).flat_map do |raw|
187
+ case raw
188
+ when String then [raw]
189
+ when Hash
190
+ keyed = raw.to_h { |key, value| [key.to_s, value] }
191
+ [keyed["gem"], keyed["id"]].compact.map(&:to_s)
192
+ else []
193
+ end
194
+ end.to_set
195
+ end
196
+ private_class_method :listed_plugin_names
197
+
198
+ # A bundled plugin's manifest id is its gem name minus the `rigor-` prefix, and `plugins:`
199
+ # accepts either spelling as `id:`, so both count as naming it.
200
+ def self.listed?(gem, listed)
201
+ require_relative "plugin/first_party"
202
+ listed.include?(gem) || listed.include?(gem.delete_prefix(Plugin::FirstParty::GEM_PREFIX))
203
+ end
204
+ private_class_method :listed?
205
+
206
+ def self.canonical_rbs_files(dir)
207
+ Dir.glob(File.join(dir, "**", "*.rbs")).filter_map { |file| canonical_path(file) }
208
+ end
209
+ private_class_method :canonical_rbs_files
210
+
211
+ # `File.realpath`, not `File.expand_path`: it resolves symlinks and, on a case-folding
212
+ # filesystem, the on-disk spelling. It raises for a path that does not exist, which is
213
+ # the answer wanted here — a path the loader cannot read is not a route to anything.
214
+ def self.canonical_path(path)
215
+ File.realpath(path.to_s)
216
+ rescue SystemCallError
217
+ nil
218
+ end
219
+ private_class_method :canonical_path
220
+
221
+ def self.under?(path, dir)
222
+ path.start_with?("#{dir}#{File::SEPARATOR}")
223
+ end
224
+ private_class_method :under?
225
+
226
+ # Whether the two directories could possibly share a file. The cheap prune that keeps the
227
+ # per-plugin globs off every run whose `signature_paths:` point at the project's own tree.
228
+ def self.overlapping?(one, other)
229
+ one == other || under?(one, other) || under?(other, one)
230
+ end
231
+ private_class_method :overlapping?
85
232
  end
86
233
  end
@@ -31,9 +31,6 @@ module Rigor
31
31
 
32
32
  # The Symbol a literal `Prism::SymbolNode` / `Prism::StringNode` names, or `nil` for any other node
33
33
  # (including `nil`).
34
- #
35
- # @param node [Prism::Node, nil]
36
- # @return [Symbol, nil]
37
34
  def symbol_or_string(node)
38
35
  return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
39
36
 
@@ -44,9 +41,6 @@ module Rigor
44
41
  # (including `nil`). The String-returning sibling of {.symbol_or_string} — for callers that key on the
45
42
  # raw name rather than the interned Symbol (route helpers, factory names, filter targets).
46
43
  # `#unescaped` round-trips an interpolation-free `:foo` / `"foo"` to `"foo"` for both kinds.
47
- #
48
- # @param node [Prism::Node, nil]
49
- # @return [String, nil]
50
44
  def symbol_or_string_name(node)
51
45
  return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
52
46
 
@@ -56,9 +50,6 @@ module Rigor
56
50
  # The Symbol a literal `Prism::SymbolNode` names, or `nil` for any other node (including a
57
51
  # `Prism::StringNode` and `nil`). Stricter than {.symbol_or_string}: a DSL that accepts only `:draft`
58
52
  # and not `"draft"` keeps that distinction by reaching for this rather than the Symbol-or-String form.
59
- #
60
- # @param node [Prism::Node, nil]
61
- # @return [Symbol, nil]
62
53
  def symbol(node)
63
54
  return nil unless node.is_a?(Prism::SymbolNode)
64
55
 
@@ -68,9 +59,6 @@ module Rigor
68
59
  # The String a literal `Prism::SymbolNode` names, or `nil` for any other node (including a
69
60
  # `Prism::StringNode` and `nil`). The String-returning sibling of {.symbol} — SymbolNode-only, but the
70
61
  # caller wants the raw name rather than the interned Symbol.
71
- #
72
- # @param node [Prism::Node, nil]
73
- # @return [String, nil]
74
62
  def symbol_name(node)
75
63
  return nil unless node.is_a?(Prism::SymbolNode)
76
64
 
@@ -79,9 +67,6 @@ module Rigor
79
67
 
80
68
  # Every literal Symbol/String positional argument of a call, in source order. Non-literal arguments
81
69
  # are dropped. Returns `[]` when the call has no argument list.
82
- #
83
- # @param call_node [Prism::CallNode, nil]
84
- # @return [Array<Symbol>]
85
70
  def symbol_arguments(call_node)
86
71
  args = call_node&.arguments&.arguments
87
72
  return [] if args.nil?
@@ -93,20 +78,12 @@ module Rigor
93
78
  # {.symbol_name} — for callers that need a predicate rather than an extraction (hash-key matching in
94
79
  # keyword or assoc argument positions, e.g. `el.is_a?(AssocNode) && symbol_named?(el.key, "required")`).
95
80
  # Uses `#unescaped` (not `#value`) for round-trip consistency.
96
- #
97
- # @param node [Prism::Node, nil]
98
- # @param name [String]
99
- # @return [Boolean]
100
81
  def symbol_named?(node, name)
101
82
  node.is_a?(Prism::SymbolNode) && node.unescaped == name
102
83
  end
103
84
 
104
85
  # The literal Symbol/String at positional `index`, or `nil` when the call has no argument list, the
105
86
  # index is out of range, or the argument there is not a literal Symbol/String.
106
- #
107
- # @param call_node [Prism::CallNode, nil]
108
- # @param index [Integer]
109
- # @return [Symbol, nil]
110
87
  def symbol_arg(call_node, index)
111
88
  args = call_node&.arguments&.arguments
112
89
  return nil if args.nil?
@@ -106,8 +106,6 @@ module Rigor
106
106
  # e.g. `node.body`). Pure and re-entrant; `break` / `next` / `return` in the block behave exactly as they
107
107
  # would with `compact_child_nodes.each`. Walkers holding a known `Prism::Node` call `#rigor_each_child`
108
108
  # directly; this wrapper is the nil-tolerant entry.
109
- #
110
- # @yieldparam child [Prism::Node]
111
109
  def each_child(node, &)
112
110
  node.rigor_each_child(&) if node.is_a?(Prism::Node)
113
111
  end
@@ -22,25 +22,19 @@ module Rigor
22
22
  class OutOfRangeError < StandardError; end
23
23
 
24
24
  class << self
25
- # @param source [String]
26
- # @param root [Prism::Node]
27
- # @param line [Integer] 1-indexed line number
28
- # @param column [Integer] 1-indexed column number (byte index within the line)
29
- # @return [Prism::Node, nil]
25
+ # @param line — 1-indexed line number
26
+ # @param column — 1-indexed column number (byte index within the line)
30
27
  def at_position(source:, root:, line:, column:)
31
28
  new(source: source, root: root).at_position(line: line, column: column)
32
29
  end
33
30
 
34
- # @param root [Prism::Node]
35
- # @param offset [Integer] 0-indexed byte offset
36
- # @return [Prism::Node, nil]
31
+ # @param offset — 0-indexed byte offset
37
32
  def at_offset(root:, offset:)
38
33
  new(source: nil, root: root).at_offset(offset)
39
34
  end
40
35
  end
41
36
 
42
- # @param source [String, nil] used by `#at_position`; may be omitted when only `#at_offset` is needed.
43
- # @param root [Prism::Node]
37
+ # @param source — used by `#at_position`; may be omitted when only `#at_offset` is needed.
44
38
  def initialize(source:, root:)
45
39
  @source = source
46
40
  @root = root
@@ -48,7 +42,7 @@ module Rigor
48
42
 
49
43
  # Resolve a `(line, column)` pair (1-indexed) to the deepest enclosing node.
50
44
  #
51
- # @raise [OutOfRangeError] if the line or column falls outside the source buffer.
45
+ # @raise OutOfRangeError — if the line or column falls outside the source buffer.
52
46
  def at_position(line:, column:)
53
47
  offset = position_to_offset(line, column)
54
48
  at_offset(offset)
@@ -25,8 +25,6 @@ module Rigor
25
25
  module NodeWalker
26
26
  module_function
27
27
 
28
- # @yieldparam node [Prism::Node]
29
- # @return [Enumerator] when no block is given.
30
28
  def each(root, &)
31
29
  return to_enum(__method__, root) unless block_given?
32
30
 
@@ -47,10 +45,6 @@ module Rigor
47
45
  # itself). The yielded `ancestors` array is the live descent stack — callers that retain it past the
48
46
  # block invocation MUST copy it (`Plugin::NodeContext` does). Used by the plugin engine to give
49
47
  # `node_rule` blocks their enclosing class / method / block context (ADR-37 slice 1d).
50
- #
51
- # @yieldparam node [Prism::Node]
52
- # @yieldparam ancestors [Array<Prism::Node>]
53
- # @return [Enumerator] when no block is given.
54
48
  def each_with_ancestors(root, &)
55
49
  return to_enum(__method__, root) unless block_given?
56
50
 
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Source
7
+ # Issue #992 — the positional-argument envelope of a `Prism::DefNode`, and the ONE place a `def` nobody
8
+ # declared is turned into something `call.wrong-arity` can check a call against.
9
+ #
10
+ # Both undeclared lanes read it. A method known only to the discovery walk has no other shape, and a
11
+ # method `rigor-rbs-inline` synthesized a fully defaulted skeleton for (`%a{rigor:v1:inferred-signature}`)
12
+ # is sent here too rather than having its synthesized member's arity read, so a `def` can never be
13
+ # judged by two parsers of its parameter list that disagree.
14
+ #
15
+ # An envelope is plain frozen data, `[min, max, required_keywords]`, because it rides the ADR-85 seed
16
+ # bundle through `Marshal`: `max` is nil when a rest or `...` parameter leaves it unbounded, and
17
+ # `required_keywords` is true when the def takes a required keyword — a call that passes positionals
18
+ # only then fails on the missing keyword whatever its count, which is a different diagnostic than the
19
+ # one this envelope backs, so the rule declines on it the way `compute_arity_envelope` declines an RBS
20
+ # function with required keywords.
21
+ #
22
+ # {OPAQUE} is the value every other contribution to a name records — `define_method`, `attr_*`, an alias,
23
+ # a macro that names the method, a second `def` whose envelope differs — and it absorbs under {.merge}.
24
+ # That makes the per-name fold a join: order-independent, idempotent, and unable to keep one of several
25
+ # shapes silently.
26
+ module ParameterEnvelope
27
+ module_function
28
+
29
+ OPAQUE = :opaque
30
+
31
+ # The envelope of `def_node`'s own parameter list. Block, keyword-optional and keyword-rest parameters
32
+ # never change how many positionals the method accepts; `super`, `yield` and `&block` in the body do
33
+ # not either, so the body is not read at all.
34
+ def of(def_node)
35
+ params = def_node.parameters
36
+ return [0, 0, false].freeze if params.nil?
37
+
38
+ required = params.requireds.size + params.posts.size
39
+ [required, maximum(params, required), required_keywords?(params)].freeze
40
+ end
41
+
42
+ def maximum(params, required)
43
+ return nil if params.rest || params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
44
+
45
+ required + params.optionals.size
46
+ end
47
+
48
+ def required_keywords?(params)
49
+ params.keywords.any?(Prism::RequiredKeywordParameterNode)
50
+ end
51
+
52
+ # The join of two contributions to one name: equal envelopes stay, anything else is {OPAQUE}.
53
+ def merge(left, right)
54
+ return right if left.nil?
55
+ return left if right.nil?
56
+
57
+ left == right ? left : OPAQUE
58
+ end
59
+
60
+ def opaque?(envelope) = envelope == OPAQUE
61
+
62
+ # Folds `overlay`'s per-class entries into `base` with {.merge}, returning a new table. Both are
63
+ # `{class name => {key => envelope}}`.
64
+ def merge_tables(base, overlay)
65
+ return overlay if base.nil? || base.empty?
66
+ return base if overlay.nil? || overlay.empty?
67
+
68
+ base.merge(overlay) { |_class, left, right| left.merge(right) { |_key, a, b| merge(a, b) } }
69
+ end
70
+ end
71
+ end
72
+ end
data/lib/rigor/source.rb CHANGED
@@ -15,3 +15,4 @@ require_relative "source/node_locator"
15
15
  require_relative "source/node_walker"
16
16
  require_relative "source/literals"
17
17
  require_relative "source/constant_path"
18
+ require_relative "source/parameter_envelope"
@@ -91,9 +91,7 @@ module Rigor
91
91
  INFO_GUARDED = %i[h5_systemic_cluster h6_genuine_bugs].freeze
92
92
  private_constant :INFO_GUARDED
93
93
 
94
- # @param diagnostics [Array<Analysis::Diagnostic>]
95
- # @param include_info [Boolean] let H5/H6 see `:info` diagnostics
96
- # @return [Array<Hint>]
94
+ # @param include_info — let H5/H6 see `:info` diagnostics
97
95
  def recognise(diagnostics, include_info: false)
98
96
  claimed = {}.compare_by_identity
99
97
  recognisers.filter_map do |recogniser|
data/lib/rigor/triage.rb CHANGED
@@ -34,11 +34,9 @@ module Rigor
34
34
  # `rbs.coverage.missing-gem`) survives; the count-based H5/H6 recognisers guard against info themselves so
35
35
  # recognition trace never reads as a bug.
36
36
  #
37
- # @param diagnostics [Array<Analysis::Diagnostic>]
38
- # @param top [Integer] hotspot-file cap
39
- # @param hints [Boolean] run the heuristic catalogue
40
- # @param include_info [Boolean] route info into the volume views
41
- # @return [Report]
37
+ # @param top — hotspot-file cap
38
+ # @param hints — run the heuristic catalogue
39
+ # @param include_info — route info into the volume views
42
40
  def analyze(diagnostics, top: 10, hints: true, include_info: false)
43
41
  routed = include_info ? diagnostics : diagnostics.reject { |d| d.severity == :info }
44
42
  Report.new(
@@ -26,9 +26,8 @@ module Rigor
26
26
 
27
27
  attr_reader :trinary, :mode, :reasons
28
28
 
29
- # @param trinary [Rigor::Trinary]
30
- # @param mode [Symbol] currently `:gradual` (default) or `:strict`.
31
- # @param reasons [Array<String>, String, nil] textual reasons; a
29
+ # @param mode — currently `:gradual` (default) or `:strict`.
30
+ # @param reasons — textual reasons; a
32
31
  # single string is wrapped, `nil` becomes an empty array.
33
32
  def initialize(trinary, mode: :gradual, reasons: nil)
34
33
  raise ArgumentError, "trinary must be Rigor::Trinary, got #{trinary.class}" unless trinary.is_a?(Trinary)
@@ -42,18 +41,37 @@ module Rigor
42
41
 
43
42
  class << self
44
43
  def yes(mode: :gradual, reasons: nil)
45
- new(Trinary.yes, mode: mode, reasons: reasons)
44
+ literal_result(Trinary.yes, mode, reasons)
46
45
  end
47
46
 
48
47
  def no(mode: :gradual, reasons: nil)
49
- new(Trinary.no, mode: mode, reasons: reasons)
48
+ literal_result(Trinary.no, mode, reasons)
50
49
  end
51
50
 
52
51
  def maybe(mode: :gradual, reasons: nil)
53
- new(Trinary.maybe, mode: mode, reasons: reasons)
52
+ literal_result(Trinary.maybe, mode, reasons)
53
+ end
54
+
55
+ private
56
+
57
+ # #775 — a result is an immutable value, so one instance serves every site that asks for the same
58
+ # verdict with the same literal reason (`"structural equality"`, `"exact name match"`, ...): the
59
+ # acceptance rules answer ~360k times on the lib self-check, most of them through a handful of
60
+ # such literals, and each answer cost the result plus its reasons array. A reason built at the
61
+ # site (an unfrozen interpolated String) or a reason list still builds a fresh result.
62
+ def literal_result(trinary, mode, reasons)
63
+ literal = reasons.nil? || (reasons.is_a?(String) && reasons.frozen?)
64
+ return new(trinary, mode: mode, reasons: reasons) unless literal
65
+
66
+ by_mode = (@literal_results[trinary] ||= {})
67
+ by_reason = (by_mode[mode] ||= {})
68
+ by_reason[reasons] ||= new(trinary, mode: mode, reasons: reasons)
54
69
  end
55
70
  end
56
71
 
72
+ # `Trinary` verdict => mode => literal reason (or nil) => the shared result.
73
+ @literal_results = {}
74
+
57
75
  def yes?
58
76
  trinary.yes?
59
77
  end