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
@@ -3,6 +3,8 @@
3
3
  require "optionparser"
4
4
 
5
5
  require_relative "../configuration"
6
+ require_relative "../effects/config_envelopes"
7
+ require_relative "../effects/registry"
6
8
  require_relative "options"
7
9
  require_relative "../sig_gen"
8
10
  require_relative "command"
@@ -22,24 +24,30 @@ module Rigor
22
24
  # AND the candidate is a `tighter-return`.
23
25
  #
24
26
  # Parameter policy defaults to `untyped`. `--params=observed` (slice 3) opts in to caller-side observation
25
- # harvesting: the `ObservationCollector` walks `--observe=PATH...` (default `spec/` when no flag is given AND a
26
- # `spec/` directory exists), unions per-position arg types, and the generator emits the union per ADR-5 clause 2.
27
+ # harvesting: the `ObservationCollector` walks `--observe=PATH...` (default: the project's test roots,
28
+ # {Configuration#resolved_test_paths}), unions per-position arg types, and the generator emits the union per ADR-5
29
+ # clause 2.
27
30
  # `--params=observed-strict` stays reserved-but-inert until the capability-role catalog ships (rejected with a usage
28
31
  # error so the surface stays stable).
29
- class SigGenCommand < Command
32
+ #
33
+ # `--check` (ADR-112 WD4) is the CI freshness gate: it runs the `--write` merge with every other flag as given,
34
+ # writes nothing, prints what would change, and exits 1 when anything would.
35
+ class SigGenCommand < Command # rubocop:disable Metrics/ClassLength
30
36
  USAGE = "Usage: rigor sig-gen [options] [paths]"
31
37
 
32
- VALID_MODES = %w[print diff write].freeze
38
+ VALID_MODES = %w[print diff write check].freeze
33
39
  VALID_PARAM_POLICIES = %w[untyped observed observed-strict].freeze
34
40
  VALID_FORMATS = %w[text json].freeze
35
41
 
36
- # The skip reasons {#report_skipped} counts. The two left out each have a detailed report of their own
37
- # ({#report_unrenderable}, {#report_unresolvable_superclasses}), so a method never shows up in two tallies.
42
+ # The skip reasons {#report_skipped} counts. The four left out each have a report of their own
43
+ # ({#report_unrenderable}, {#report_unresolvable_superclasses}, {#report_inline_declared}, and the refusal
44
+ # lines `--write` / `--check` print for `inline_differs`), so a method never shows up in two tallies.
38
45
  SUMMARISED_SKIP_REASONS = (SigGen::Classification::SKIP_DIAGNOSTIC_IDS.keys -
39
- %i[unrenderable_rbs unresolvable_superclass]).freeze
46
+ %i[unrenderable_rbs unresolvable_superclass inline_declared
47
+ inline_differs]).freeze
40
48
  private_constant :SUMMARISED_SKIP_REASONS
41
49
 
42
- # @return [Integer] CLI exit status.
50
+ # @return CLI exit status.
43
51
  def run
44
52
  options = parse_options
45
53
  return CLI::EXIT_USAGE if options.nil?
@@ -50,17 +58,22 @@ module Rigor
50
58
  observations = collect_observations(configuration, options)
51
59
  generator = SigGen::Generator.new(configuration: configuration, paths: paths,
52
60
  observations: observations,
53
- include_private: options.fetch(:include_private))
61
+ include_private: options.fetch(:include_private),
62
+ effect_annotator: effect_annotator(configuration, paths, options),
63
+ overwrite: options.fetch(:overwrite))
54
64
  candidates = generator.run
55
65
  mode = options.fetch(:mode).to_sym
56
66
 
57
- status = if mode == :write
58
- dispatch_write(candidates, configuration, options)
67
+ status = case mode
68
+ when :write then dispatch_write(candidates, configuration, options)
69
+ when :check then dispatch_check(candidates, configuration, options)
59
70
  else
60
71
  dispatch_print_or_diff(candidates, mode, options)
61
72
  0
62
73
  end
63
74
  report_skipped(candidates, options)
75
+ report_inline_declared(candidates, options)
76
+ report_withheld_annotations(candidates, options)
64
77
  report_unrenderable(generator.unrenderable)
65
78
  report_unresolvable_superclasses(generator.unresolvable_superclasses)
66
79
  status
@@ -68,6 +81,74 @@ module Rigor
68
81
 
69
82
  private
70
83
 
84
+ # ADR-103 WD9 — the effect table sig-gen writes annotations from, or `nil`.
85
+ #
86
+ # The gate is the project's own `effects:` opt-in and nothing else. An annotation is read back as an
87
+ # enforced envelope, so turning emission on from a sig-gen flag alone would let one command commit a
88
+ # project to a contract `rigor check` was never asked to keep; and a project that has not opted in
89
+ # pays neither the analysis this needs nor a single changed byte of output.
90
+ #
91
+ # `--effect-envelopes` is the second, narrower switch: `%a{pure}` is the ecosystem's existing purity
92
+ # spelling and round-trips through Steep as well as Rigor, while `%a{rigor:v1:effect …}` is Rigor's
93
+ # own and belongs in a project's `sig/` only when the author asked for it by name.
94
+ def effect_annotator(configuration, paths, options)
95
+ unless configuration.effects_enabled?
96
+ if options.fetch(:effect_envelopes)
97
+ @err.puts("rigor sig-gen: --effect-envelopes needs the `effects:` opt-in in .rigor.yml; " \
98
+ "no effect annotation was emitted.")
99
+ end
100
+ return nil
101
+ end
102
+
103
+ require_relative "check_runner_factory"
104
+ # Through the same factory `rigor check` and `rigor doctor` use, so the LRU cap, the worker
105
+ # resolution and the tolerated-effects switch cannot drift from the command whose cache this run
106
+ # shares. `workers: 0` because a collecting run is pinned to the sequential path anyway, and
107
+ # `--no-cache` mirrors `rigor check`'s flag of the same name.
108
+ runner = CheckRunnerFactory.build(
109
+ configuration: configuration,
110
+ options: { no_cache: options.fetch(:no_cache), explain: false, stats: false, workers: 0 },
111
+ buffer: nil, cache_root: configuration.cache_path
112
+ )
113
+ runner.run((configuration.paths + paths).uniq)
114
+ SigGen::EffectAnnotation::Annotator.new(
115
+ table: runner.effect_table, envelopes: options.fetch(:effect_envelopes),
116
+ envelope_index: runner.effect_envelopes, config_envelopes: config_envelopes(configuration)
117
+ )
118
+ end
119
+
120
+ # The project's `effects.envelopes:` entries, for gate 0's `match:` half. Built with a plain
121
+ # registry rather than the run's: gate 0 asks only WHETHER an entry selects this class, never what
122
+ # it bounds, so the vocabulary a plugin would add cannot change the answer.
123
+ def config_envelopes(configuration)
124
+ Effects::ConfigEnvelopes.build(
125
+ entries: configuration.effects_envelopes,
126
+ registry: Effects::Registry.for_configuration(configuration)
127
+ )
128
+ rescue StandardError
129
+ []
130
+ end
131
+
132
+ # The withheld half of the emission, counted the way {#report_skipped} counts a skip: a method that
133
+ # did NOT get `%a{pure}` is the interesting case for a reader who expected one, and silence would
134
+ # read as "sig-gen does not do this" rather than "this method did not earn it".
135
+ def report_withheld_annotations(candidates, options)
136
+ return unless options.fetch(:format) == "text"
137
+
138
+ counts = candidates.each_with_object(Hash.new(0)) do |candidate, acc|
139
+ reason = candidate.effect_reason
140
+ acc[reason] += 1 if reason && reason != :emitted
141
+ end
142
+ return if counts.empty?
143
+
144
+ breakdown = counts.map { |reason, n| "#{SigGen::EffectAnnotation::DIAGNOSTIC_IDS.fetch(reason)}: #{n}" }
145
+ @err.puts(
146
+ "rigor sig-gen: withheld an effect annotation from #{counts.values.sum} method(s) " \
147
+ "(#{breakdown.join(', ')}). An annotation is enforced once written, so it is emitted only " \
148
+ "from an exhaustive, undischarged summary."
149
+ )
150
+ end
151
+
71
152
  # Issue #778 — one stderr line per run saying how many methods the generator declined and why, so a
72
153
  # method missing from the output is never a silent absence. Text mode only: under `--format=json` every
73
154
  # skipped row is already in the payload with its `skip_reason`, and stderr stays clean for the consumer.
@@ -90,6 +171,18 @@ module Rigor
90
171
  )
91
172
  end
92
173
 
174
+ # ADR-112 WD4 — `sig_gen.inline_declared: skip` leaving methods out is the project's own choice, not a
175
+ # method sig-gen "could not type or would not overwrite", so it is counted on a line of its own.
176
+ def report_inline_declared(candidates, options)
177
+ return unless options.fetch(:format) == "text"
178
+
179
+ count = candidates.count { |c| c.skip_reason == :inline_declared }
180
+ return if count.zero?
181
+
182
+ @err.puts("rigor sig-gen: left #{count} method(s) declared inline out of sig/, as " \
183
+ "`sig_gen.inline_declared: skip` asks (sig.skipped.inline-declared).")
184
+ end
185
+
93
186
  # A method whose rendered RBS does not parse is a Rigor rendering defect, not a fact about the user's
94
187
  # code — the generator skipped it (so the rest of the signatures are still usable and still valid), but
95
188
  # staying silent would leave the user with a quietly incomplete `sig/` and us with an unreported bug.
@@ -135,32 +228,101 @@ module Rigor
135
228
  )
136
229
  end
137
230
 
138
- # @return [Integer] exit status — non-zero when a file the user asked to write could not be written.
231
+ # @return exit status — non-zero when a file the user asked to write could not be written.
139
232
  def dispatch_write(candidates, configuration, options)
140
- layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
141
- path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
142
- writer = SigGen::Writer.new(path_mapper: path_mapper, overwrite: options.fetch(:overwrite))
143
-
144
- results = writer.write_all(candidates)
233
+ results = build_writer(configuration, options).write_all(candidates)
145
234
 
146
- SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format))
147
- # A refused write (an assembled file that does not parse, or an existing target that is not valid
148
- # UTF-8) means the user asked for a write and did not get one, so the command must not report
149
- # success — a green `sig-gen --write` in CI would otherwise mean nothing.
235
+ refused = refused_candidates(candidates)
236
+ SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format), refused: refused)
237
+ SigGen::Renderer.refusal_lines(refused).each { |line| @err.puts(line) }
238
+ # A refused write (an assembled file that does not parse, an existing target that is not valid UTF-8,
239
+ # or a method whose inline and `sig/` declarations disagree without `--overwrite`) means the user asked for a
240
+ # write and did not get one, so the command must not report success — a green `sig-gen --write` in CI
241
+ # would otherwise mean nothing.
150
242
  refusals = %i[skipped_invalid_rbs skipped_invalid_encoding]
151
- results.any? { |result| refusals.include?(result.action) } ? 1 : 0
243
+ results.any? { |result| refusals.include?(result.action) } || !refused.empty? ? 1 : 0
244
+ end
245
+
246
+ # ADR-112 WD4 — the methods whose inline and `sig/` declarations disagree, refused because the run did not
247
+ # pass `--overwrite`. Unlike every other skip, `sig/` contradicts the source while one stands.
248
+ def refused_candidates(candidates)
249
+ candidates.select { |candidate| candidate.skip_reason == :inline_differs }
250
+ end
251
+
252
+ # ADR-112 WD4 — the same writer as {#dispatch_write}, flags included, in dry-run mode. Defined by what
253
+ # `--write` would do rather than by whether `--diff` prints anything: a tighter return against an existing
254
+ # declaration is a proposal `--write` declines without `--overwrite`, and a gate that counted it could
255
+ # never pass on a project that reviewed it and said no. With `--overwrite` it counts, because `--write
256
+ # --overwrite` would apply it.
257
+ #
258
+ # @return 1 when `sig/` is out of date (or a write would be refused), 0 when it is current.
259
+ def dispatch_check(candidates, configuration, options)
260
+ results = build_writer(configuration, options, dry_run: true).write_all(candidates)
261
+ refused = refused_candidates(candidates)
262
+ SigGen::Renderer.new(out: @out).render_check(results: results, format: options.fetch(:format),
263
+ refused: refused)
264
+ stale = SigGen::Renderer.out_of_date(results)
265
+ return 0 if stale.empty? && refused.empty?
266
+
267
+ report_check_failure(stale, refused) if options.fetch(:format) == "text"
268
+ 1
269
+ end
270
+
271
+ def report_check_failure(stale, refused)
272
+ unless stale.empty?
273
+ @err.puts("rigor sig-gen --check: #{stale.size} signature file(s) out of date; " \
274
+ "run `rigor sig-gen --write` with the same options to update them.")
275
+ end
276
+ return if refused.empty?
277
+
278
+ @err.puts("rigor sig-gen --check: #{refused.size} method(s) whose inline declaration and sig/ copy " \
279
+ "disagree; make them agree, or pass --overwrite to replace the sig/ member.")
280
+ end
281
+
282
+ def build_writer(configuration, options, dry_run: false)
283
+ layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
284
+ path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
285
+ SigGen::Writer.new(path_mapper: path_mapper, overwrite: options.fetch(:overwrite), dry_run: dry_run)
152
286
  end
153
287
 
154
288
  # Slice 3 — collect call-site argument observations when `--params=observed` is set. When `--observe=PATH` is not
155
- # specified, default to `spec/` (skipped silently when the directory is absent).
289
+ # specified, observe the project's test roots: `test_paths:`, or whichever of `spec/` and `test/` exist. With no
290
+ # root at all the run still succeeds, but says so — otherwise every parameter stays `untyped` with nothing
291
+ # naming the reason.
156
292
  def collect_observations(configuration, options)
157
293
  return {} if options.fetch(:params) != "observed"
158
294
 
159
295
  observe_paths = options.fetch(:observe)
160
- observe_paths = ["spec"] if observe_paths.empty? && File.directory?("spec")
296
+ observe_paths = configuration.resolved_test_paths if observe_paths.empty?
297
+ if observe_paths.empty?
298
+ warn_no_test_roots(configuration)
299
+ else
300
+ warn_missing_test_roots(observe_paths)
301
+ end
161
302
  SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths).collect
162
303
  end
163
304
 
305
+ # A declared root that does not exist is read as empty, so name it: `sig-gen` is the only command that reads
306
+ # the test roots, and without this the run looks exactly like one whose tests pass no typed arguments.
307
+ def warn_missing_test_roots(observe_paths)
308
+ missing = observe_paths.reject { |path| File.exist?(path) }
309
+ return if missing.empty?
310
+
311
+ consequence = missing.size == observe_paths.size ? "; every parameter stays untyped" : ""
312
+ @err.puts("rigor sig-gen: no call sites are observed from #{missing.map(&:inspect).join(', ')}, " \
313
+ "which does not exist#{consequence}. Check `test_paths:` or --observe=PATH.")
314
+ end
315
+
316
+ def warn_no_test_roots(configuration)
317
+ reason = if configuration.test_paths.nil?
318
+ "no spec/ or test/ directory was found and `test_paths:` is unset"
319
+ else
320
+ "`test_paths:` is empty"
321
+ end
322
+ @err.puts("rigor sig-gen: --params=observed has no test roots to observe (#{reason}); every parameter " \
323
+ "stays untyped. Declare `test_paths:` in the configuration or pass --observe=PATH.")
324
+ end
325
+
164
326
  def parse_options
165
327
  options = {
166
328
  mode: "print",
@@ -170,6 +332,8 @@ module Rigor
170
332
  overwrite: false,
171
333
  observe: [],
172
334
  include_private: false,
335
+ effect_envelopes: false,
336
+ no_cache: false,
173
337
  config: nil
174
338
  }
175
339
  build_option_parser(options).parse!(@argv)
@@ -181,23 +345,33 @@ module Rigor
181
345
  nil
182
346
  end
183
347
 
184
- def build_option_parser(options) # rubocop:disable Metrics/AbcSize
348
+ def build_option_parser(options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
185
349
  OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
186
350
  opts.banner = USAGE
187
- opts.on("--print", "Write RBS skeletons to stdout (default)") { options[:mode] = "print" }
188
- opts.on("--diff", "Write a unified diff against existing RBS") { options[:mode] = "diff" }
189
- opts.on("--write", "Write generated RBS to sig/<path>.rbs files") { options[:mode] = "write" }
190
- opts.on("--overwrite", "Allow tighter-return updates to replace user-authored RBS") do
351
+ opts.on("--print", "Write RBS skeletons to stdout (default)") { select_mode(options, "print") }
352
+ opts.on("--diff", "Write a unified diff against existing RBS") { select_mode(options, "diff") }
353
+ opts.on("--write", "Write generated RBS to sig/<path>.rbs files") { select_mode(options, "write") }
354
+ opts.on("--check", "Exit 1 when --write would change sig/; write nothing") { select_mode(options, "check") }
355
+ opts.on("--overwrite", "Allow tighter-return updates, and inline declarations that disagree with sig/, " \
356
+ "to replace user-authored RBS") do
191
357
  options[:overwrite] = true
192
358
  end
193
359
  opts.on("--include-private", "Emit private / protected instance methods (default: public only)") do
194
360
  options[:include_private] = true
195
361
  end
362
+ opts.on("--effect-envelopes", "Also emit %a{rigor:v1:effect ...} for effectful methods " \
363
+ "(requires the effects: opt-in)") do
364
+ options[:effect_envelopes] = true
365
+ end
366
+ opts.on("--no-cache", "Do not read or write the analysis cache (effect collection only)") do
367
+ options[:no_cache] = true
368
+ end
196
369
  opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
197
370
  opts.on("--params=POLICY", "Parameter policy: untyped (default), observed, observed-strict") do |value|
198
371
  options[:params] = value
199
372
  end
200
- opts.on("--observe=PATH", "Directory / file to scan for call-site observations (repeatable)") do |value|
373
+ opts.on("--observe=PATH", "Directory / file to scan for call-site observations (repeatable; " \
374
+ "default: the configured test_paths)") do |value|
201
375
  options[:observe] << value
202
376
  end
203
377
  opts.on("--new-files", "Emit only new-file classifications") do
@@ -213,12 +387,19 @@ module Rigor
213
387
  end
214
388
  end
215
389
 
390
+ # A second, different mode flag is a usage error rather than last-one-wins: `--check --write` must not
391
+ # quietly write in a CI job that meant to gate, nor `--write --check` quietly not.
392
+ def select_mode(options, mode)
393
+ options[:mode] = options[:mode_given] && options[:mode] != mode ? "conflict" : mode
394
+ options[:mode_given] = true
395
+ end
396
+
216
397
  def validation_error(options)
217
398
  mode = options.fetch(:mode)
218
399
  format = options.fetch(:format)
219
400
  params = options.fetch(:params)
220
401
 
221
- return "--print, --diff, and --write are mutually exclusive flags; pick one" unless VALID_MODES.include?(mode)
402
+ return "--print, --diff, --write, and --check are mutually exclusive flags; pick one" if mode == "conflict"
222
403
  return "unsupported --format=#{format}" unless VALID_FORMATS.include?(format)
223
404
  return "unsupported --params=#{params}" unless VALID_PARAM_POLICIES.include?(params)
224
405
  if params == "observed-strict"
@@ -74,9 +74,9 @@ module Rigor
74
74
 
75
75
  # The bundled skills live at `<gem_root>/skills/`. From `lib/rigor/cli/skill_command.rb` that is three directories
76
76
  # up.
77
- SKILLS_ROOT = File.expand_path("../../../skills", __dir__)
77
+ SKILLS_ROOT = File.expand_path("../../../skills", __dir__).freeze
78
78
 
79
- # @return [Integer] CLI exit status.
79
+ # @return CLI exit status.
80
80
  def run
81
81
  case @argv.first
82
82
  when nil
@@ -37,22 +37,22 @@ module Rigor
37
37
  # that could not run is NOT a clean check and the report must never let it read as one.
38
38
  # - `route` / `reason` — the headline override, or nil to leave the presence-only recommendation standing.
39
39
  Report = Data.define(:status, :detail, :route, :reason) do
40
- # @return [Boolean] true when the check did not complete — the caller must then say so rather than let the
40
+ # @return true when the check did not complete — the caller must then say so rather than let the
41
41
  # fallback recommendation read as an all-clear.
42
42
  def failed?
43
43
  status == :error
44
44
  end
45
45
  end
46
46
 
47
- # @param config [String, nil] the config filename the presence probe found, relative to `root`.
48
- # @param root [String] project root (the analysis itself resolves paths against the process cwd, as
47
+ # @param config — the config filename the presence probe found, relative to `root`.
48
+ # @param root — project root (the analysis itself resolves paths against the process cwd, as
49
49
  # `rigor check` does).
50
50
  def initialize(config:, root: Dir.pwd)
51
51
  @config = config
52
52
  @root = root
53
53
  end
54
54
 
55
- # @return [Report] never raises — every failure degrades to a `:error`/`:skipped` report whose `route` is nil,
55
+ # @return never raises — every failure degrades to a `:error`/`:skipped` report whose `route` is nil,
56
56
  # leaving the presence-only headline in charge.
57
57
  def run
58
58
  if @config.nil?
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "yaml"
4
4
 
5
+ require_relative "../plugin_gap_advisory"
6
+
5
7
  module Rigor
6
8
  class CLI
7
9
  # The presence-only project-state probe behind `rigor skill describe` (ADR-73 WD2). It stats files and opens only
@@ -17,19 +19,11 @@ module Rigor
17
19
  # RBS into the signature paths, so its presence is the signal that the gem-RBS gap has been addressed.
18
20
  RBS_COLLECTION_LOCKFILE = "rbs_collection.lock.yaml"
19
21
 
20
- # `Gemfile.lock` substrings that mark a Rails app, and the bundled Rails-family plugin ids — used to spot a
21
- # configured Rails project that has not enabled any Rails plugin (a `rigor-plugin-tune` cue).
22
- RAILS_LOCK_MARKERS = %w[railties actionpack activerecord actioncable].freeze
23
- RAILS_PLUGIN_MARKERS = %w[
24
- rigor-activerecord rigor-actionpack rigor-actionmailer rigor-activejob
25
- rigor-rails-routes rigor-rails-i18n rigor-actioncable rigor-activestorage rigor-rails
26
- ].freeze
27
-
28
22
  def initialize(root)
29
23
  @root = root
30
24
  end
31
25
 
32
- # @return [Hash] the presence-only state the recommendation routes on.
26
+ # @return the presence-only state the recommendation routes on.
33
27
  def to_h
34
28
  config = CONFIG_FILENAMES.find { |name| File.file?(File.join(@root, name)) }
35
29
  {
@@ -38,7 +32,7 @@ module Rigor
38
32
  sig: File.directory?(File.join(@root, "sig")),
39
33
  gems: File.file?(File.join(@root, "Gemfile.lock")),
40
34
  rbs_collection: File.file?(File.join(@root, RBS_COLLECTION_LOCKFILE)),
41
- rails_unconfigured: rails_unconfigured?(config),
35
+ plugins_unconfigured: plugins_unconfigured?(config),
42
36
  ci: ci_state,
43
37
  editor: editor_state,
44
38
  mcp: mcp_state
@@ -47,24 +41,26 @@ module Rigor
47
41
 
48
42
  private
49
43
 
50
- # True when Rails is in `Gemfile.lock` but the (present) config enables no Rails plugin — so `rigor-plugin-tune`
51
- # (wiring the ActiveRecord / routes / i18n plugins) buys more than community RBS would (the 20260620 field trial's
52
- # strap case). Only fires on an already-configured project; an un-configured Rails app routes to
53
- # `rigor-project-init`, which selects the plugins itself.
54
- def rails_unconfigured?(config)
44
+ # ADR-96 WD2 — true when the project depends on gems that bundled plugins model and enables none of those
45
+ # plugins, so `rigor-plugin-tune` buys more than community RBS would (the 20260620 field trial's strap
46
+ # case). Reads each plugin's `target_gems:` rather than the Rails-only table it replaces, so the cue
47
+ # covers every framework Rigor ships a plugin for. Only fires on an already-configured project; an
48
+ # un-configured app routes to `rigor-project-init`, which selects the plugins itself.
49
+ def plugins_unconfigured?(config)
55
50
  return false if config.nil?
56
51
 
57
- lock = File.join(@root, "Gemfile.lock")
58
- return false unless File.file?(lock) && file_mentions_any?(lock, RAILS_LOCK_MARKERS)
59
-
60
- !file_mentions_any?(File.join(@root, config), RAILS_PLUGIN_MARKERS)
52
+ plugins = configured_plugins(File.join(@root, config))
53
+ PluginGapAdvisory.unconfigured?(project_root: @root, plugins: plugins)
61
54
  end
62
55
 
63
- def file_mentions_any?(path, markers)
64
- content = File.read(path)
65
- markers.any? { |marker| content.include?(marker) }
56
+ # The probe reads the config file itself rather than {Rigor::Configuration}: it must stay a
57
+ # presence-only probe, and `Configuration.load` applies defaults (including the ADR-93 auto-wired
58
+ # `rigor-rbs-inline` entry) that have nothing to do with the question asked here.
59
+ def configured_plugins(path)
60
+ data = YAML.safe_load_file(path, permitted_classes: [], aliases: true)
61
+ data.is_a?(Hash) ? Array(data["plugins"]) : []
66
62
  rescue StandardError
67
- false
63
+ []
68
64
  end
69
65
 
70
66
  # `:wired` (a CI config mentions `rigor`), `:unwired` (a CI config exists but does not), or `:none`.
@@ -121,9 +117,9 @@ module Rigor
121
117
  # The entry-point SKILL itself — excluded from the catalogue because it is the skill being run, not a destination.
122
118
  ENTRY_POINT_SKILL = "rigor-next-steps"
123
119
 
124
- # Adoption-journey order for the catalogue and the order the recommendation decision tree walks. `rigor-ask` sits
125
- # last: it is the journey-agnostic "answer a question about Rigor" companion the agent can offer at any point,
126
- # never a presence-recommended step.
120
+ # Adoption-journey order for the catalogue and the order the recommendation decision tree walks. The last two are
121
+ # journey-agnostic companions the agent can offer at any point, never presence-recommended steps:
122
+ # `rigor-type-oracle` fires on an event (being about to write a type), and `rigor-ask` on a question about Rigor.
127
123
  CATALOG_ORDER = %w[
128
124
  rigor-project-init
129
125
  rigor-rbs-setup
@@ -137,12 +133,13 @@ module Rigor
137
133
  rigor-plugin-author
138
134
  rigor-upgrade
139
135
  rigor-doctor
136
+ rigor-type-oracle
140
137
  rigor-ask
141
138
  ].freeze
142
139
 
143
- # @param skills [Array<Hash>] discovered skills, each `{name:, path:}`.
144
- # @param root [String] project root to probe (defaults to the cwd).
145
- # @param deep [Boolean] opt into `--deep`: run a real `rigor check` and let its result pick the headline. Off by
140
+ # @param skills — discovered skills, each `{name:, path:}`.
141
+ # @param root — project root to probe (defaults to the cwd).
142
+ # @param deep — opt into `--deep`: run a real `rigor check` and let its result pick the headline. Off by
146
143
  # default, and off is ADR-73 WD2's contract — see {SkillDeepProbe} for why the whole analysis path lives
147
144
  # behind this one flag and in its own file.
148
145
  def initialize(skills:, root: Dir.pwd, deep: false)
@@ -151,7 +148,6 @@ module Rigor
151
148
  @deep = deep
152
149
  end
153
150
 
154
- # @return [String] the full describe report.
155
151
  def render
156
152
  catalog = catalog_skills
157
153
  state = ProjectStateProbe.new(@root).to_h
@@ -202,14 +198,18 @@ module Rigor
202
198
  def recommended_name_and_reason(state)
203
199
  if state.fetch(:config).nil?
204
200
  ["rigor-project-init", "this project has no Rigor configuration yet — start here."]
205
- elsif state.fetch(:rails_unconfigured)
201
+ elsif state.fetch(:plugins_unconfigured)
206
202
  ["rigor-plugin-tune",
207
- "Rails is in your Gemfile.lock but no Rails plugins are enabled — wire them so " \
208
- "ActiveRecord / routes / i18n calls resolve (a bigger win here than community RBS)."]
209
- elsif state.fetch(:gems) && !state.fetch(:rbs_collection)
210
- ["rigor-rbs-setup", "your gems ship no community RBS yet — install it so Rigor stops typing them as Dynamic."]
203
+ "your Gemfile declares gems Rigor ships plugins for and none is enabled — wire them so " \
204
+ "those framework calls resolve (a bigger win here than community RBS)."]
205
+ # ADR-73's second `rbs-setup` priority-softening case (2026-09-10): CI wiring is presence-only and
206
+ # side-effect-free, while community-RBS install is network-bound (`rbs collection install` hits
207
+ # rubygems.org). A configured project earns the cheap, local win first — checked ahead of the RBS
208
+ # gap rather than after it.
211
209
  elsif state.fetch(:ci) != :wired
212
210
  ["rigor-ci-setup", "Rigor is configured but not wired into CI — lock in the regression guard."]
211
+ elsif state.fetch(:gems) && !state.fetch(:rbs_collection)
212
+ ["rigor-rbs-setup", "your gems ship no community RBS yet — install it so Rigor stops typing them as Dynamic."]
213
213
  # A present baseline is deliberately NOT a recommendation trigger. A baseline is a healthy, finished onboarding
214
214
  # state, not a problem to work off; pushing every baselined project to "reduce it" turns a working build into a
215
215
  # chore and tempts scattering `# rigor:disable` through the code to make a number go down — means over ends.
@@ -12,7 +12,7 @@ require_relative "../inference/flow_tracer"
12
12
  require_relative "../inference/scope_indexer"
13
13
  require_relative "command"
14
14
  require_relative "trace_renderer"
15
- require_relative "probe_environment"
15
+ require_relative "../project_environment"
16
16
 
17
17
  module Rigor
18
18
  class CLI
@@ -30,7 +30,7 @@ module Rigor
30
30
  DEFAULT_KINDS = %i[bind union dispatch].freeze
31
31
  VERBOSE_KINDS = (%i[enter result] + DEFAULT_KINDS).freeze
32
32
 
33
- # @return [Integer] CLI exit status.
33
+ # @return CLI exit status.
34
34
  def run
35
35
  options = parse_options
36
36
  file = @argv.first
@@ -92,10 +92,10 @@ module Rigor
92
92
  end
93
93
 
94
94
  # Mirrors the single-file path `rigor type-of` takes: the plugin-aware environment (so a trace replays the
95
- # same dispatch `rigor check` would, including plugin-synthesized RBS — see {ProbeEnvironment}), an empty
95
+ # same dispatch `rigor check` would, including plugin-synthesized RBS — see {ProjectEnvironment}), an empty
96
96
  # seed scope, one statement-level evaluation of the whole program — but recorded under the FlowTracer.
97
97
  def record_events(root, file, configuration)
98
- environment = ProbeEnvironment.build(configuration: configuration, source_files: [file])
98
+ environment = ProjectEnvironment.build(configuration: configuration, source_files: [file])
99
99
  scope = Scope.empty(environment: environment, source_path: file)
100
100
  Inference::FlowTracer.record { scope.evaluate(root) }
101
101
  end
@@ -28,9 +28,8 @@ module Rigor
28
28
  BODY_HEIGHT_MIN = 3 # never shrink the source window below this
29
29
  DEFAULT_SIZE = [24, 80].freeze
30
30
 
31
- # @param out [IO]
32
- # @param source [String] the traced file's source.
33
- # @param file [String] display path.
31
+ # @param source — the traced file's source.
32
+ # @param file — display path.
34
33
  def initialize(out:, source:, file:)
35
34
  @out = out
36
35
  @source = source
@@ -38,11 +37,11 @@ module Rigor
38
37
  @lines = source.lines.map(&:chomp)
39
38
  end
40
39
 
41
- # @param events [Array<Rigor::Inference::FlowTracer::Event>] the
40
+ # @param events — the
42
41
  # pre-filtered frame list (the command owns kind filtering).
43
- # @param delay [Float, nil] seconds between frames (autoplay);
42
+ # @param delay — seconds between frames (autoplay);
44
43
  # nil = step on key press when interactive.
45
- # @param interactive [Boolean] whether to clear/redraw and wait.
44
+ # @param interactive — whether to clear/redraw and wait.
46
45
  def play(events, delay: nil, interactive: false)
47
46
  @rows, @cols = interactive ? terminal_size : DEFAULT_SIZE
48
47
  @interactive = interactive
@@ -22,7 +22,7 @@ module Rigor
22
22
  USAGE = "Usage: rigor triage [options] [paths]"
23
23
  DEFAULT_SECTIONS = %i[distribution selectors hotspots hints].freeze
24
24
 
25
- # @return [Integer] CLI exit status (always 0).
25
+ # @return CLI exit status (always 0).
26
26
  def run
27
27
  options = parse_options
28
28
  configuration = Configuration.load(options.fetch(:config))