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
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../inference/rbs_type_translator"
4
+ require_relative "type_elaborator"
5
+
6
+ module Rigor
7
+ module SigGen
8
+ # Issue #1002 — the WRITE direction of a project's own type aliases.
9
+ #
10
+ # `Generator`'s `alias_expander:` wiring (PR #1000) reads a declared alias: `-> Type::t` in the project's
11
+ # `sig/` becomes the 22-member union so a proposal can be compared against it. Nothing went the other way,
12
+ # so a proposal that inferred exactly that union printed all 22 members — output an author cannot paste,
13
+ # because review rewrites it back into the alias the project already declares.
14
+ #
15
+ # This index answers the reverse lookup: given the RBS a proposal erased to, is there a project-declared
16
+ # alias whose own expansion erases to the same member SET, close enough to the method's own namespace to
17
+ # be the name a reader expects? The comparison is deliberately a set, not a string: a union missing one
18
+ # arm is a different type and must keep printing in full.
19
+ #
20
+ # A wrong alias name is worse than a long union — it is an RBS claim the author did not make and may not
21
+ # hold — so every rule below refuses rather than guesses.
22
+ #
23
+ # - **Project aliases only.** The candidate `.rbs` files come from the configuration's OWN resolved
24
+ # `signature_paths:` (what `Environment.for_project` calls `resolved_paths`), passed in by the caller.
25
+ # Deliberately NOT `RbsLoader#signature_paths`: that is `resolved_paths` PLUS plugin signature paths,
26
+ # bundler-discovered gem `sig/` directories, the `rbs collection` tree and Rigor's own gem overlays.
27
+ # Keyed off the loader, a project that merely bundles a gem shipping RBS aliases would find that gem's
28
+ # vocabulary in its proposals. A loaded plugin's own `sig/` is subtracted even when the project wired
29
+ # it through `signature_paths:` itself (the #697 shape) — it is the plugin's vocabulary either way.
30
+ # - **Lossless alias bodies only.** Neither translation nor erasure is injective. `Type::Intersection`
31
+ # erases to its FIRST member, `Refined` and `Difference` to their base, a proc type translates to a
32
+ # bare `Proc`, and an RBS intersection may not even survive translation. An alias
33
+ # `type inter = (Integer & Comparable) | String` therefore reaches the renderer indistinguishable from
34
+ # `Integer | String`, and folding a proposal that really is `Integer | String` into `inter` would claim
35
+ # an intersection the method never returns — RBS an author cannot accept. So the alias body is checked
36
+ # on BOTH sides of translation, each an ALLOW-list ({.lossless_rbs?} over the RBS AST,
37
+ # {.lossless?} over the translated carriers). Anything absent — including a form added after this was
38
+ # written, and a nested alias reference, whose own body this walk does not see — disqualifies the
39
+ # alias. Refusing to fold is always safe; naming the wrong type is not.
40
+ # - **Namespace proximity.** Type equality is not enough to make a name right for the reader.
41
+ # `:positive | :negative` is type-equal to this repository's `Analysis::FactStore::polarity` from
42
+ # anywhere in the tree, and naming it inside an unrelated class is a worse proposal than the union. An
43
+ # alias is only offered to a method whose owner is inside the alias's own namespace, and the NEAREST
44
+ # such alias wins.
45
+ # - **Unions only.** An alias whose expansion is a single type (`type name = String`) is never folded;
46
+ # the noise the issue is about is specifically the long union.
47
+ # - **Non-generic only.** A generic alias's expansion depends on its arguments, so there is no fixed
48
+ # member set to key on.
49
+ #
50
+ # A project with no aliases builds an empty index, and an empty index is a no-op: `#fold` returns its
51
+ # argument, so the rendered output is byte-identical to the pre-#1002 output.
52
+ class AliasIndex
53
+ # The carriers whose `erase_to_rbs` is faithful enough to key on. Everything absent is disqualifying,
54
+ # which is why this is a list of names resolved lazily rather than a `case`: a new carrier is excluded
55
+ # until someone establishes its erasure round-trips.
56
+ LOSSLESS_CARRIERS = %i[Top Bot Constant Nominal Singleton Union Tuple Maybe IntegerRange FloatRange].freeze
57
+ private_constant :LOSSLESS_CARRIERS
58
+
59
+ # A proc type translates to a bare `Proc` nominal, losing the signature, so an alias mentioning one
60
+ # cannot be keyed on its erasure either.
61
+ PROC_CLASS_NAMES = ["Proc", "::Proc"].freeze
62
+ private_constant :PROC_CLASS_NAMES
63
+
64
+ # The RBS forms whose meaning survives translation AND erasure. Notable absences, each a form that
65
+ # reaches the renderer looking like something narrower or wider than it is: `Intersection`, `Proc`,
66
+ # `Record`, `Interface`, `Alias` (its body is expanded during translation, out of this walk's sight),
67
+ # `untyped` / `void` / `top`, and every context-dependent base (`self`, `instance`, `class`).
68
+ LOSSLESS_RBS_TYPES = %w[
69
+ RBS::Types::ClassInstance RBS::Types::ClassSingleton RBS::Types::Union RBS::Types::Optional
70
+ RBS::Types::Tuple RBS::Types::Literal RBS::Types::Bases::Nil RBS::Types::Bases::Bool
71
+ RBS::Types::Bases::Bottom
72
+ ].freeze
73
+ private_constant :LOSSLESS_RBS_TYPES
74
+
75
+ # @return an index with no entries — {#fold} is the identity.
76
+ def self.empty
77
+ new({})
78
+ end
79
+
80
+ # @param signature_paths — the project's OWN resolved signature paths.
81
+ # @param excluded_paths — signature paths that reached the list from somewhere other than the
82
+ # author: a loaded plugin's own `sig/`, which #697 lets a project wire through `signature_paths:`
83
+ # as well. Those aliases are the plugin's vocabulary, not this project's.
84
+ # Fail-soft in the ADR-5 sense: a loader that cannot enumerate aliases, and any alias whose body fails
85
+ # to translate, yields an entry-free index rather than an error, because a rendering nicety must never
86
+ # fail a run.
87
+ def self.build(environment:, signature_paths:, excluded_paths: [])
88
+ loader = environment&.rbs_loader
89
+ return empty unless loader.respond_to?(:each_type_alias_decl)
90
+
91
+ project_files = project_sig_files(signature_paths) - project_sig_files(excluded_paths)
92
+ return empty if project_files.empty?
93
+
94
+ new(collect_entries(loader, project_files, environment))
95
+ end
96
+
97
+ # Every `.rbs` file under the given signature paths, absolute. Directories only, matching `RbsLoader`,
98
+ # which skips a `signature_paths:` entry that is not a directory.
99
+ def self.project_sig_files(signature_paths)
100
+ Array(signature_paths).flat_map do |path|
101
+ dir = path.is_a?(Pathname) ? path : Pathname(path.to_s)
102
+ next [] unless dir.directory?
103
+
104
+ Dir.glob(dir.join("**", "*.rbs").to_s).map { |p| File.expand_path(p) }
105
+ end.to_set
106
+ end
107
+
108
+ # `{ member set => entries sorted by declaration position }`. Every alias matching a set is kept, not
109
+ # just one: which of them is the right NAME depends on the method being rendered, and that is only
110
+ # known at {#fold} time.
111
+ def self.collect_entries(loader, project_files, environment)
112
+ entries = Hash.new { |h, k| h[k] = [] }
113
+ loader.each_type_alias_decl do |type_name, decl_entry|
114
+ entry = build_entry(loader, project_files, environment, type_name, decl_entry)
115
+ entries[entry[:members]] << entry unless entry.nil?
116
+ end
117
+ entries.each_value { |list| list.sort_by! { |entry| entry[:order] } }
118
+ entries.default_proc = nil
119
+ entries
120
+ end
121
+
122
+ def self.build_entry(loader, project_files, environment, type_name, decl_entry)
123
+ decl = decl_entry.respond_to?(:decl) ? decl_entry.decl : nil
124
+ return nil if decl.nil?
125
+ return nil if decl.respond_to?(:type_params) && !Array(decl.type_params).empty?
126
+
127
+ file = declaration_file(decl)
128
+ return nil unless file && project_files.include?(file)
129
+
130
+ members = expanded_members(loader, environment, decl)
131
+ return nil if members.nil?
132
+
133
+ # The ABSOLUTE spelling. RBS resolves a relative type name innermost-first, so a bare
134
+ # `Deep::Inner::mood` written inside `Deep::Inner` would rebind if the project also declared a
135
+ # `Deep::Inner::Deep`. The leading `::` says exactly which alias the proposal means.
136
+ relative = type_name.to_s.delete_prefix("::")
137
+ { members: members, name: "::#{relative}", namespace: relative.split("::")[0..-2].join("::"),
138
+ order: [file, declaration_line(decl), relative] }
139
+ end
140
+
141
+ def self.declaration_file(decl)
142
+ buffer_name = decl.location&.buffer&.name
143
+ return nil if buffer_name.nil?
144
+
145
+ File.expand_path(buffer_name.to_s)
146
+ rescue StandardError
147
+ nil
148
+ end
149
+
150
+ def self.declaration_line(decl)
151
+ decl.location&.start_line || 0
152
+ rescue StandardError
153
+ 0
154
+ end
155
+
156
+ # nil whenever the alias is not usable as a rendering target: an untranslatable body, a body carrying a
157
+ # carrier whose erasure loses information, a body that erases to `untyped` (the gradual-consistency
158
+ # collapse in `Type::Union#erase_to_rbs`, which would otherwise key every such alias on the same
159
+ # one-member set), or a single-type body.
160
+ def self.expanded_members(loader, environment, decl)
161
+ return nil unless lossless_rbs?(decl.type)
162
+
163
+ translated = Inference::RbsTypeTranslator.translate(
164
+ decl.type, self_type: nil, instance_type: nil, type_vars: {}, alias_expander: loader
165
+ )
166
+ return nil if translated.nil? || !lossless?(translated)
167
+
168
+ erased = TypeElaborator.elaborate(translated, environment: environment).erase_to_rbs
169
+ members = split_top_level(erased)
170
+ members.size < 2 ? nil : members
171
+ rescue StandardError
172
+ nil
173
+ end
174
+
175
+ # The RBS-AST half of the check, and the one that catches an intersection: `(Integer & Comparable)`
176
+ # does not necessarily survive translation as a `Type::Intersection`, so by the time the carrier walk
177
+ # runs there is nothing left to notice.
178
+ def self.lossless_rbs?(rbs_type)
179
+ return false unless LOSSLESS_RBS_TYPES.include?(rbs_type.class.name)
180
+ return true unless rbs_type.respond_to?(:each_type)
181
+
182
+ children = []
183
+ rbs_type.each_type { |child| children << child }
184
+ children.all? { |child| lossless_rbs?(child) }
185
+ end
186
+
187
+ # The carrier half, a backstop for what translation itself invents (a `Refined` or `Difference` from a
188
+ # `rigor:` envelope, a bare `Proc` from a proc type reached some other way). Walks the TRANSLATED
189
+ # tree, so it also sees a construct the RBS walk could not reach.
190
+ def self.lossless?(type)
191
+ carrier = type.class.name.to_s.split("::").last&.to_sym
192
+ return false unless LOSSLESS_CARRIERS.include?(carrier)
193
+ return false if carrier == :Nominal && PROC_CLASS_NAMES.include?(type.class_name.to_s)
194
+
195
+ children_of(type).all? { |child| lossless?(child) }
196
+ end
197
+
198
+ def self.children_of(type)
199
+ case type
200
+ when Type::Union then type.members
201
+ when Type::Tuple then type.elements
202
+ when Type::Maybe then [type.value_type]
203
+ when Type::Nominal then type.type_args
204
+ else []
205
+ end
206
+ end
207
+
208
+ # Splits an erased RBS string on its TOP-LEVEL `|` separators. Depth tracking keeps a nested union
209
+ # (`Array[A | B]`) whole, and the quote state keeps a literal string type (`"a | b"`) whole.
210
+ def self.split_top_level(rendered)
211
+ members = []
212
+ current = +""
213
+ depth = 0
214
+ quote = nil
215
+ rendered.each_char do |ch|
216
+ if quote
217
+ current << ch
218
+ quote = nil if ch == quote
219
+ next
220
+ end
221
+
222
+ case ch
223
+ when '"', "'" then quote = ch
224
+ when "(", "[", "{" then depth += 1
225
+ when ")", "]", "}" then depth -= 1
226
+ when "|"
227
+ if depth.zero?
228
+ members << current.strip
229
+ current = +""
230
+ next
231
+ end
232
+ end
233
+ current << ch
234
+ end
235
+ members << current.strip
236
+ members.reject(&:empty?).to_set
237
+ end
238
+
239
+ # The namespaces a method owned by `owner` can name an alias from, INNERMOST FIRST: its own namespace,
240
+ # then each enclosing one, ending at the top level. `nil`/`""` for a top-level owner still reaches the
241
+ # top-level namespace, which is where an adopting project's `type` declarations usually live.
242
+ def self.namespace_chain(owner)
243
+ segments = owner.to_s.split("::")
244
+ segments.size.downto(0).map { |i| segments[0...i].join("::") }
245
+ end
246
+
247
+ def initialize(entries)
248
+ @entries = entries
249
+ end
250
+
251
+ # @param owner — the class or module the rendered member belongs to; `nil` renders unchanged,
252
+ # because proximity cannot be judged without it.
253
+ # @return the alias name when `rendered` is exactly some in-scope project alias's expansion,
254
+ # else `rendered` unchanged. Callers can apply this unconditionally.
255
+ def fold(rendered, owner)
256
+ return rendered if @entries.empty? || owner.nil? || !rendered.include?("|")
257
+
258
+ members = self.class.split_top_level(rendered)
259
+ return rendered if members.size < 2
260
+
261
+ candidates = @entries[members]
262
+ return rendered if candidates.nil? || candidates.empty?
263
+
264
+ nearest(candidates, owner) || rendered
265
+ end
266
+
267
+ def empty?
268
+ @entries.empty?
269
+ end
270
+
271
+ private
272
+
273
+ # "Most specific, then declaration order", the rule the issue asked to have stated and pinned. The
274
+ # chain index is the distance from the method's own namespace outwards, so an alias declared beside the
275
+ # method beats one declared in an enclosing namespace, and `candidates` is already in declaration order
276
+ # ((file, line, name)), which `min_by` keeps as the tie-break. An alias whose namespace does not
277
+ # enclose the owner at all is not a candidate: it names the right type under a name this reader has no
278
+ # reason to expect.
279
+ def nearest(candidates, owner)
280
+ chain = self.class.namespace_chain(owner)
281
+ best = candidates.each_with_index.filter_map do |entry, position|
282
+ distance = chain.index(entry[:namespace])
283
+ distance.nil? ? nil : [[distance, position], entry[:name]]
284
+ end.min_by(&:first)
285
+ best&.last
286
+ end
287
+ end
288
+ end
289
+ end
@@ -2,8 +2,9 @@
2
2
 
3
3
  module Rigor
4
4
  module SigGen
5
- # The five classifications a candidate method falls into after the generator has compared the inferred
6
- # return type against the project's existing RBS.
5
+ # The classifications a candidate method falls into after the generator has compared the inferred return
6
+ # type — or, for a member declared inline, the inline declaration (ADR-112 WD4) — against the project's
7
+ # existing RBS.
7
8
  #
8
9
  # The strings are the diagnostic-family identifiers ADR-14 reserves under `sig.*`; the MVP carries them as
9
10
  # plain symbols on the method candidate and renders the matching identifier in JSON / text output. They are
@@ -15,16 +16,22 @@ module Rigor
15
16
  TIGHTER_RETURN = :tighter_return
16
17
  EQUIVALENT = :equivalent
17
18
  SKIPPED = :skipped
19
+ # ADR-112 WD4 — a member declared inline by `# @rbs` / `#:` whose `sig/` declaration disagrees with it,
20
+ # under `--overwrite`: the whole `sig/` member is replaced by the inline line. Produced only when the run
21
+ # asked for `--overwrite`; without it the same member is refused (`sig.skipped.inline-differs`), because
22
+ # neither side is presumed right and a slot-by-slot mix of two declarations need not be a declaration.
23
+ INLINE_OVERWRITE = :inline_overwrite
18
24
 
19
25
  # The classifications that actually produce a line in a generated `sig/`. Consulted by the renderer,
20
26
  # the writer and the generator's own post-passes; it lived as a private constant in the first two,
21
27
  # which is one fork too many for a list this load-bearing.
22
- EMITTABLE = [NEW_FILE, NEW_METHOD, TIGHTER_RETURN].freeze
28
+ EMITTABLE = [NEW_FILE, NEW_METHOD, TIGHTER_RETURN, INLINE_OVERWRITE].freeze
23
29
 
24
30
  DIAGNOSTIC_IDS = {
25
31
  NEW_FILE => "sig.generated.new-file",
26
32
  NEW_METHOD => "sig.generated.new-method",
27
- TIGHTER_RETURN => "sig.generated.tighter-return"
33
+ TIGHTER_RETURN => "sig.generated.tighter-return",
34
+ INLINE_OVERWRITE => "sig.generated.inline-overwrite"
28
35
  }.freeze
29
36
 
30
37
  SKIP_DIAGNOSTIC_IDS = {
@@ -40,7 +47,17 @@ module Rigor
40
47
  unresolvable_superclass: "sig.skipped.unresolvable-superclass",
41
48
  # Issue #744 — a project subclass overrides this method and its override is NOT emitted, so the
42
49
  # declaration would be inherited by a subclass it does not describe.
43
- overridden_by_unsigned_subclass: "sig.skipped.overridden-by-unsigned-subclass"
50
+ overridden_by_unsigned_subclass: "sig.skipped.overridden-by-unsigned-subclass",
51
+ # ADR-112 WD4 — `sig_gen.inline_declared: skip` is set and the inline reader declares this member, so
52
+ # a `sig/` copy would be a second declaration of it for a Steep that reads the inline annotations too.
53
+ inline_declared: "sig.skipped.inline-declared",
54
+ # ADR-112 WD4 — the member's class, or one it is nested in, is generic by an inline declaration (`# @rbs
55
+ # generic T`) and `sig/` does not declare it yet. sig-gen writes no class type parameters, and a header
56
+ # without them fails the class's definition build.
57
+ inline_generic_class: "sig.skipped.inline-generic-class",
58
+ # ADR-112 WD4 — the member is declared inline and in `sig/`, and the two disagree. A refusal, not a
59
+ # skip: `--write` and `--check` exit 1 until a person reconciles them or passes `--overwrite`.
60
+ inline_differs: "sig.skipped.inline-differs"
44
61
  }.freeze
45
62
  end
46
63
  end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbs"
4
+
5
+ module Rigor
6
+ module SigGen
7
+ # ADR-112 WD4 — whether a method's inline declaration and its `sig/` declaration say the same thing, so that
8
+ # `rigor sig-gen` refuses (or, under `--overwrite`, replaces) only a real disagreement (#1076).
9
+ #
10
+ # Two declarations are compared as types, not text. What is ignored, because RBS gives it no meaning:
11
+ #
12
+ # - parameter names (`(String)` and `(String s)`);
13
+ # - how a union is spelled: member order, `T?` for `T | nil`, `bool` for `true | false`, nesting;
14
+ # - a name written absolute or relative, when both resolve to the same constant — `::Foo` and `Foo` inside
15
+ # `module NS` are the same only if `NS` declares no `Foo` of its own ({#initialize}'s `resolve`).
16
+ #
17
+ # What is kept: overload order (RBS picks the first overload that matches, so `A | B` and `B | A` can
18
+ # answer a call differently), method type parameters as named, and everything else about the shape.
19
+ class DeclarationEquivalence
20
+ # @param resolve — maps a type name as written (`String`, `::Foo`, `Bar::Baz`) to the name it denotes,
21
+ # in the namespace both declarations sit in.
22
+ def initialize(resolve:)
23
+ @resolve = resolve
24
+ end
25
+
26
+ def same?(left, right)
27
+ left.size == right.size && left.zip(right).all? { |a, b| method_type(a) == method_type(b) }
28
+ end
29
+
30
+ # The overloads with their returns left out, for comparing what an author wrote against a member whose
31
+ # return is inferred.
32
+ def same_parameters?(left, right)
33
+ left.size == right.size && left.zip(right).all? do |a, b|
34
+ method_type(a, with_return: false) == method_type(b, with_return: false)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def method_type(method_type, with_return: true)
41
+ [
42
+ method_type.type_params.map { |param| param.name.to_s },
43
+ function(method_type.type, with_return: with_return),
44
+ block(method_type.block)
45
+ ]
46
+ end
47
+
48
+ def block(block)
49
+ return nil if block.nil?
50
+
51
+ [block.required, function(block.type), block.self_type && type(block.self_type)]
52
+ end
53
+
54
+ def function(function, with_return: true)
55
+ returned = with_return ? type(function.return_type) : nil
56
+ return [:untyped_function, returned] if function.is_a?(::RBS::Types::UntypedFunction)
57
+
58
+ [
59
+ function.required_positionals.map { |param| type(param.type) },
60
+ function.optional_positionals.map { |param| type(param.type) },
61
+ function.rest_positionals && type(function.rest_positionals.type),
62
+ function.trailing_positionals.map { |param| type(param.type) },
63
+ keywords(function.required_keywords),
64
+ keywords(function.optional_keywords),
65
+ function.rest_keywords && type(function.rest_keywords.type),
66
+ returned
67
+ ]
68
+ end
69
+
70
+ def keywords(map)
71
+ map.map { |name, param| [name.to_s, type(param.type)] }.sort
72
+ end
73
+
74
+ def type(node)
75
+ case node
76
+ when ::RBS::Types::Optional, ::RBS::Types::Union, ::RBS::Types::Bases::Bool then union(node)
77
+ when ::RBS::Types::ClassInstance then [:instance, @resolve.call(node.name.to_s), types(node.args)]
78
+ when ::RBS::Types::ClassSingleton then [:singleton, @resolve.call(node.name.to_s)]
79
+ when ::RBS::Types::Interface, ::RBS::Types::Alias then named(node)
80
+ else composite(node)
81
+ end
82
+ end
83
+
84
+ # An interface or alias is compared by its written name, a leading `::` aside: those names are rarely
85
+ # nested, and the environment question this class asks of a class name has no counterpart for them.
86
+ def named(node)
87
+ [:named, node.name.to_s.delete_prefix("::"), types(node.args)]
88
+ end
89
+
90
+ def composite(node)
91
+ case node
92
+ when ::RBS::Types::Tuple then [:tuple, types(node.types)]
93
+ when ::RBS::Types::Record then [:record, record_fields(node)]
94
+ when ::RBS::Types::Intersection then [:intersection, types(node.types).sort_by(&:inspect)]
95
+ when ::RBS::Types::Proc then [:proc, function(node.type), block(node.block)]
96
+ else [:other, node.to_s]
97
+ end
98
+ end
99
+
100
+ def types(nodes)
101
+ nodes.map { |node| type(node) }
102
+ end
103
+
104
+ def record_fields(node)
105
+ required = node.fields.map { |key, field| [key.to_s, true, type(field)] }
106
+ optional = node.respond_to?(:optional_fields) ? node.optional_fields : {}
107
+ (required + optional.map { |key, field| [key.to_s, false, type(field)] }).sort_by(&:inspect)
108
+ end
109
+
110
+ def union(node)
111
+ members = union_members(node).uniq.sort_by(&:inspect)
112
+ members.size == 1 ? members.first : [:union, members]
113
+ end
114
+
115
+ # `true` / `false` / `nil` canonicalise as their literal spellings do (`[:other, "true"]`), so `bool` and
116
+ # `true | false`, or `T?` and `T | nil`, land on the same members.
117
+ def union_members(node)
118
+ case node
119
+ when ::RBS::Types::Union then node.types.flat_map { |member| union_members(member) }
120
+ when ::RBS::Types::Optional then union_members(node.type) + [[:other, "nil"]]
121
+ when ::RBS::Types::Bases::Bool then [[:other, "true"], [:other, "false"]]
122
+ else [type(node)]
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../effects/config_envelopes"
4
+ require_relative "../effects/label_set"
5
+ require_relative "../effects/method_key"
6
+ require_relative "../effects/summary"
7
+
8
+ module Rigor
9
+ module SigGen
10
+ # ADR-103 WD9 / ADR-14's reserved annotation-emission slot — decides whether a generated signature
11
+ # carries `%a{pure}` or `%a{rigor:v1:effect …}`, and records WHY when it does not.
12
+ #
13
+ # The whole module is one decision applied per method, and every branch of it exists to keep a WRONG
14
+ # annotation off the page. An emitted annotation is not a hint: the effects opt-in reads it back as an
15
+ # envelope and enforces it on the method and on everything the method reaches, so a `%a{pure}` sig-gen
16
+ # invented is a false contract that manufactures `effect.envelope-exceeded` on correct code. That is why
17
+ # the gates below are all one-directional — each of them can only suppress an emission.
18
+ #
19
+ # The gates, in order:
20
+ #
21
+ # 0. **Already declared** — the method's own signature, its class's, an `effects.envelopes:` entry
22
+ # (by `namespace:` OR by `match:`), or an rbs-inline `# @rbs %a{…}` states a bound. The author has
23
+ # spoken and sig-gen has nothing to add; writing its own answer over that would silently replace a
24
+ # contract with an inference. `withheld-declared`. This is a DIFFERENT lane from gate 5: an
25
+ # envelope on the method ITSELF goes to {Effects::EnvelopeCheck} and never enters the summary's
26
+ # `≤` lane, so the table cannot see it. An annotation carrying an unknown label counts: it reads
27
+ # as ⊤ and bounds nothing, but the author still wrote about this method, and replacing their typo
28
+ # with `%a{pure}` would delete the only thing `effect.unknown-label` has to point at.
29
+ # 1. **No summary** — the method is not an effect unit this run collected. Nothing is emitted and nothing
30
+ # is reported: the absence is about the run, not about the method.
31
+ # 2. **Non-exhaustive** ({Effects::Summary#exhaustive?} false) — the summary reads "these effects, and
32
+ # possibly more", which is precisely the claim an envelope must not make. `withheld-non-exhaustive`.
33
+ # 3. **Policy-discharged** — the transitive proven lane and the `effects.tolerated:` judgment disagree,
34
+ # so some part of this method's footprint is only invisible because the project agreed to ignore it.
35
+ # `docs/type-specification/effect-labels.md` § Discharge by policy, invariant 4 ("emission uses
36
+ # undischarged sets") and ADR-10 WD7's "opportunistic shapes never round-trip" are the same rule:
37
+ # a tolerated `telemetry` origin does not earn a written `%a{pure}`. `withheld-tolerated`.
38
+ # 4. **Unclaimed callee** ({Effects::EffectTable::Entry#unclaimed?}) — the method, or something it
39
+ # reaches, called something NOTHING described: no catalogue row, no plugin row, no envelope, and no
40
+ # project definition the closure could read. The summary is exhaustive, because every call
41
+ # RESOLVED — but "every call resolved" and "every callee's footprint is known" are different
42
+ # questions, and only the second one licenses a written bound. `withheld-unclaimed-callee`.
43
+ # 5. **A surviving declared label** ({Effects::EffectTable::Entry#trivial?}) — the proven lane is empty
44
+ # and the `≤` lane is not, which is a method whose callee stated a bound the analyzer never proved
45
+ # away. `%a{pure}` there would contradict a claim the project already carries.
46
+ # `withheld-declared`.
47
+ # 6. **Proven pure** — exhaustive, undischarged, claimed, and nothing outside `{mutate.local}` in
48
+ # either lane. `%a{pure}`, the ecosystem's existing purity spelling (design § 6.3), which hands
49
+ # Steep users better narrowing.
50
+ # 7. **Proven effectful** — the same, with real labels. `%a{rigor:v1:effect …}` only when the caller
51
+ # asked for envelopes; otherwise nothing, because the labelled spelling is Rigor's own and writing
52
+ # it into a project's `sig/` unbidden is a bigger commitment than a purity tag.
53
+ module EffectAnnotation
54
+ # The `sig.*` telemetry identifiers for this emission, alongside {Classification::DIAGNOSTIC_IDS} and
55
+ # {Classification::SKIP_DIAGNOSTIC_IDS}. Documented in `docs/type-specification/diagnostic-policy.md`
56
+ # and `docs/handbook/11-sig-gen.md` with the rest of the `sig.*` family.
57
+ DIAGNOSTIC_IDS = {
58
+ # An annotation was rendered onto the proposal.
59
+ emitted: "sig.effect.emitted",
60
+ # Withheld: the proven footprint and the `effects.tolerated:` judgment disagree.
61
+ withheld_tolerated: "sig.effect.withheld-tolerated",
62
+ # Withheld: some call this method reaches could not be resolved.
63
+ withheld_non_exhaustive: "sig.effect.withheld-non-exhaustive",
64
+ # Withheld: some call it reaches resolved, and nothing anywhere says what that callee does.
65
+ withheld_unclaimed_callee: "sig.effect.withheld-unclaimed-callee",
66
+ # Withheld: the `≤` lane carries a label the proven lane does not already admit.
67
+ withheld_declared: "sig.effect.withheld-declared",
68
+ # Write-time: the target declaration already carries annotations, so its bytes were left alone.
69
+ left_unreadable: "sig.effect.left-unreadable"
70
+ }.freeze
71
+
72
+ module_function
73
+
74
+ # The effect-unit key for a sig-gen candidate, in {Effects::MethodKey}'s spelling.
75
+ def key_for(class_name, method_name, kind)
76
+ return nil if class_name.nil?
77
+
78
+ "#{class_name}#{kind == :singleton ? '.' : '#'}#{method_name}"
79
+ end
80
+
81
+ # Turns one {Effects::EffectTable::Entry} into the annotation lines to render and the reason to report.
82
+ #
83
+ # @param declared — whether the method already carries an authored bound of its own
84
+ # ({Effects::EnvelopeIndex}); see gate 0.
85
+ # @return `[Array<String> annotations, Symbol|nil reason]`. An empty
86
+ # array with a `nil` reason is "nothing to say about this method".
87
+ def decide(entry, envelopes: false, declared: false)
88
+ return [[], :withheld_declared] if declared
89
+ return [[], nil] if entry.nil?
90
+ return [[], :withheld_non_exhaustive] unless entry.exhaustive?
91
+ return [[], :withheld_tolerated] unless entry.proven == entry.undischarged
92
+ return [[], :withheld_unclaimed_callee] if entry.unclaimed?
93
+ # `trivial?` is the report's own "nothing to say about this method" test, and it is the emission
94
+ # test too: exhaustive, nothing beyond `mutate.local` proven, and NOTHING SURVIVING in the `≤`
95
+ # lane. The last conjunct is what a bare `proven.subsumed_by?` misses — a callee's imported
96
+ # envelope lands in the declared lane with no taint and no proven label, so a method whose whole
97
+ # body is one `Remote.fetch` reads `[] ≤ [io.net.http]` while proving nothing at all.
98
+ return [["%a{pure}"], :emitted] if entry.trivial?
99
+ return [[], :withheld_declared] if entry.proven.subsumed_by?(Effects::Summary::TRIVIAL_BOUND)
100
+ return [[], nil] unless envelopes
101
+ # A surviving declared label is not spelled either. The envelope grammar carries ONE bound per
102
+ # declaration and has no way to say "proves this, claims that", so writing the proven set alone
103
+ # would publish a bound narrower than the claim the project already carries.
104
+ return [[], :withheld_declared] unless entry.rendered_declared.empty?
105
+ # `top?` is the unbounded reading: it names no labels, so there is no envelope to spell.
106
+ return [[], nil] if entry.proven.top? || entry.proven.empty?
107
+
108
+ [["%a{rigor:v1:effect #{entry.proven.to_a.join(', ')}}"], :emitted]
109
+ end
110
+
111
+ # Per-run lookup over an {Effects::EffectTable}. Built by the CLI when the project's `effects:` opt-in
112
+ # is on, and `nil` otherwise — which is what makes effects-off output byte-identical to a run before
113
+ # this feature existed.
114
+ class Annotator
115
+ NO_ENTRIES = [].freeze
116
+ private_constant :NO_ENTRIES
117
+
118
+ # @param envelope_index — the run's {Effects::EnvelopeIndex}, so
119
+ # gate 0 can see a bound the author already wrote. `nil` skips that half of the gate.
120
+ # @param config_envelopes — the project's
121
+ # `effects.envelopes:` entries. Passed SEPARATELY from the index because the index drops every
122
+ # entry without a `namespace:` (it serves call-site import, and a `match:` glob is a fact about
123
+ # where a class is defined that a per-file collection window cannot see). Emission can see it:
124
+ # a sig-gen candidate carries the defining file it came from, which is exactly what
125
+ # {Effects::ConfigEnvelopes.selects?} matches a `match:` entry against.
126
+ def initialize(table:, envelopes: false, envelope_index: nil, config_envelopes: NO_ENTRIES)
127
+ @table = table
128
+ @envelopes = envelopes
129
+ @envelope_index = envelope_index
130
+ @config_envelopes = config_envelopes
131
+ freeze
132
+ end
133
+
134
+ # @param path — the `.rb` file the def came from, for a `match:`-selected
135
+ # `effects.envelopes:` entry.
136
+ # @return `[Array<String>, Symbol|nil]` — see {EffectAnnotation.decide}.
137
+ def annotate(class_name:, method_name:, kind:, path: nil)
138
+ key = EffectAnnotation.key_for(class_name, method_name, kind)
139
+ return [[], nil] if key.nil?
140
+
141
+ EffectAnnotation.decide(
142
+ @table[key], envelopes: @envelopes,
143
+ declared: declared?(class_name, method_name, kind, path)
144
+ )
145
+ end
146
+
147
+ private
148
+
149
+ def declared?(class_name, method_name, kind, path)
150
+ annotated?(class_name, method_name, kind) || config_selected?(class_name, path)
151
+ end
152
+
153
+ def annotated?(class_name, method_name, kind)
154
+ return false if @envelope_index.nil?
155
+
156
+ @envelope_index.annotated?(class_name, kind == :singleton, method_name.to_s)
157
+ end
158
+
159
+ # `namespace:` entries are answered by the index too; asking here as well costs one scan over a
160
+ # handful of entries and keeps the `match:` half from being a second, differently-shaped answer.
161
+ def config_selected?(class_name, path)
162
+ return false if @config_envelopes.empty? || class_name.nil?
163
+
164
+ @config_envelopes.any? { |entry| Effects::ConfigEnvelopes.selects?(entry, class_name, match_paths(path)) }
165
+ end
166
+
167
+ # Every spelling of the candidate's file a `match:` glob might be written against.
168
+ #
169
+ # A glob is matched `Dir.pwd`-relative (`ConfigEnvelopes.for_classes` defaults `project_root:` to
170
+ # the same thing, so the two sides agree and both agree with `rigor check`), while a candidate's
171
+ # path is whatever the invocation named: relative when `paths:` supplied it, absolute when the
172
+ # user typed one, and neither when it arrives through a symlink — `sig-gen alias/presented.rb`
173
+ # where `alias` links to `lib` is a real invocation, and the glob its author wrote says `lib/`.
174
+ #
175
+ # All three are tried rather than one canonical form. Normalising to the real path alone would
176
+ # break a project whose own `paths:` entry is the symlink (the glob would then be written against
177
+ # the link), and normalising to `Dir.pwd` alone is what let the symlink through.
178
+ def match_paths(path)
179
+ [path, relative_path(path), relative_path(real_path(path))].compact.uniq
180
+ end
181
+
182
+ def relative_path(path)
183
+ return nil if path.nil?
184
+
185
+ expanded = File.expand_path(path.to_s)
186
+ root = "#{File.expand_path(Dir.pwd)}#{File::SEPARATOR}"
187
+ expanded.start_with?(root) ? expanded.delete_prefix(root) : nil
188
+ end
189
+
190
+ # Resolved through every symlink on the way, or nil when the file is gone — a candidate names a
191
+ # file the generator just read, so a miss here is a race rather than a shape to handle.
192
+ def real_path(path)
193
+ return nil if path.nil?
194
+
195
+ File.realpath(path.to_s)
196
+ rescue SystemCallError
197
+ nil
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end